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:
@@ -111,8 +111,9 @@ unit tests (24) + box integration tests (2).
|
|||||||
validate (accuracy/ECE on a held-out slice, prompt-hash regression), swap the
|
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
|
pinned model revision. GPU offload: train on a beefier GPU; the running agent
|
||||||
keeps a frozen inference revision until a swap validates.
|
keeps a frozen inference revision until a swap validates.
|
||||||
- `create_skill` branch: invoke opencode to author a skill manifest at a tree
|
- `create_skill` / `create_category` branches: navigation logs a suggestion event
|
||||||
leaf (currently a stub that only logs the request).
|
(state, query, SemIf output) to the trace — currently a stub; opencode
|
||||||
|
authoring at a tree leaf is deferred.
|
||||||
- Queue persistence (durable across restarts).
|
- Queue persistence (durable across restarts).
|
||||||
- Event/timer intake sources beyond typed input.
|
- Event/timer intake sources beyond typed input.
|
||||||
- Concurrency: SemIf shared-state mode (`score_shared` / `SerialPrefixScorer`)
|
- Concurrency: SemIf shared-state mode (`score_shared` / `SerialPrefixScorer`)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ All decisions are SemIf calls: `{state, question, options[]}`. State is the curr
|
|||||||
|
|
||||||
- **`choice`** — binary: `interrupt` / `defer`. Interrupt iff `P(interrupt) >= τ`.
|
- **`choice`** — binary: `interrupt` / `defer`. Interrupt iff `P(interrupt) >= τ`.
|
||||||
- **`score`** — ordinal urgency: `critical` / `high` / `medium` / `low`, mapped to numeric weights for sorting.
|
- **`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").
|
- **`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.
|
**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.
|
- 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.
|
- 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
|
### Skill manifest
|
||||||
- name, category, description, allowed inputs, action list, cost budget, decision log reference.
|
- name, category, description, allowed inputs, action list, cost budget, decision log reference.
|
||||||
|
|||||||
@@ -198,12 +198,11 @@ class Scheduler:
|
|||||||
# ---- dispatch ----
|
# ---- dispatch ----
|
||||||
|
|
||||||
def _dispatch(self, request: Request) -> DispatchResult:
|
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):
|
if isinstance(navigation, CreateSkill):
|
||||||
self.trace.append("create_skill", request.id, category=navigation.category)
|
|
||||||
return DispatchResult(
|
return DispatchResult(
|
||||||
kind="create_skill",
|
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)
|
outcome = self.runner.run(navigation, request)
|
||||||
if outcome.error:
|
if outcome.error:
|
||||||
|
|||||||
+41
-8
@@ -1,8 +1,9 @@
|
|||||||
"""The skill tree, registry, and SemIf-driven navigation.
|
"""The skill tree, registry, and SemIf-driven navigation.
|
||||||
|
|
||||||
A skill is a leaf reached by a chain of SemIf choices (category -> skill).
|
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
|
The category level carries a "create_category" branch and the leaf level a
|
||||||
there (deferred to v2, stubbed as CreateSkill).
|
"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.
|
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 .decisions import DecisionRequest, Option, Request
|
||||||
from .engine import SemIfEngine
|
from .engine import SemIfEngine
|
||||||
from .log import DecisionLog
|
from .log import DecisionLog
|
||||||
|
from .trace import TraceLog
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -56,7 +58,12 @@ class Skill:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CreateSkill:
|
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
|
category: str | None = None
|
||||||
|
|
||||||
@@ -162,32 +169,58 @@ def build_tree(skills: list[Skill]) -> dict[str, list[Skill]]:
|
|||||||
def navigate(
|
def navigate(
|
||||||
engine: SemIfEngine,
|
engine: SemIfEngine,
|
||||||
log: DecisionLog,
|
log: DecisionLog,
|
||||||
|
trace: TraceLog,
|
||||||
request: Request,
|
request: Request,
|
||||||
tree: dict[str, list[Skill]],
|
tree: dict[str, list[Skill]],
|
||||||
) -> Skill | CreateSkill:
|
) -> 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())
|
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(
|
top = DecisionRequest(
|
||||||
state=compose_state(request),
|
state=compose_state(request),
|
||||||
question="Which top-level category handles this 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)
|
top_result = engine.call(top)
|
||||||
log.append(top, top_result, extra={"phase": "navigate:category", "run_id": request.id})
|
log.append(top, top_result, extra={"phase": "navigate:category", "run_id": request.id})
|
||||||
category = top_result.selected
|
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)
|
return CreateSkill(category=None)
|
||||||
skills = tree[category]
|
skills = tree[category]
|
||||||
|
create_skill = Option("create_skill", "Suggest creating a new skill.")
|
||||||
leaf = DecisionRequest(
|
leaf = DecisionRequest(
|
||||||
state=compose_state(request, current=category),
|
state=compose_state(request, current=category),
|
||||||
question=f"Within {category}, which skill?",
|
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)
|
leaf_result = engine.call(leaf)
|
||||||
log.append(leaf, leaf_result, extra={"phase": "navigate:leaf", "run_id": request.id})
|
log.append(leaf, leaf_result, extra={"phase": "navigate:leaf", "run_id": request.id})
|
||||||
pick = leaf_result.selected
|
pick = leaf_result.selected
|
||||||
if pick == "create_skill":
|
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 CreateSkill(category=category)
|
||||||
return next(s for s in skills if s.name == pick)
|
return next(s for s in skills if s.name == pick)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user