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.
115 lines
7.4 KiB
Markdown
115 lines
7.4 KiB
Markdown
# Semif Agent
|
|
|
|
A local desktop CLI agent whose control flow is a single decision model (SemIf). Inputs are gated, scored, queued, and dispatched through a skill tree; every decision is logged as a labeled training row, and the decision model is fine-tuned at regular intervals on the accumulated prediction-vs-observation cost.
|
|
|
|
## Engine: SemIf (local decision layer)
|
|
|
|
- **SemIf** (formerly OpenJev, TheoLeeCJ, MIT) is a local rebuild of Jev's interface pattern. One forward pass reads typed option logits directly from a model; no answer sentence, no decoding loop.
|
|
- Contract: input `{state, question, options[]}`, output per-option probabilities conditional on the supplied options. Probabilities are conditional on the option set, not absolute confidence — calibrate per workload (SemIf ships per-workload temperature scaling).
|
|
- **Why local**: desktop target, data stays in-network (SemIf's data boundary), runs on a home GPU (or CPU via llama.cpp). Jev (hosted) is not in scope.
|
|
- Decision model is pinned to an exact revision; every swap is auditable via prompt hashes in SemIf's output.
|
|
|
|
## Core loop
|
|
|
|
```
|
|
intake (typed / events / timers / other skills)
|
|
└─ contains_request? (gate: is this even actionable?)
|
|
└─ choice (SemIf: "should this interrupt the current process?")
|
|
├─ yes → preempt current, requeue it with state preserved
|
|
└─ no → score (SemIf: "how urgent?")
|
|
└─ priority queue, sorted by urgency desc
|
|
```
|
|
|
|
- **Scheduler**: run head of queue when idle. A new interrupt displaces the current process, which itself requeues preserving its state.
|
|
- **Concurrency** (TODO resolved): single execution slot for v1. SemIf's shared-state mode (one prefetched state branched across many criteria in parallel) is the lever for parallelizing decisions later, not concurrent process execution.
|
|
- **Tie-breaking**: FIFO within equal urgency, recency as secondary key.
|
|
- **Priority ageing**: queued items decay upward over time so they can't starve.
|
|
- **Bounds**: max queue depth; overflow rejects with a notification.
|
|
|
|
## Decision templates
|
|
|
|
All decisions are SemIf calls: `{state, question, options[]}`. State is the current process + input. Options are typed with descriptions.
|
|
|
|
- **`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; 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.
|
|
|
|
## Skill tree
|
|
|
|
- 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 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.
|
|
|
|
## Skill anatomy
|
|
|
|
Every skill run follows the same loop:
|
|
|
|
1. **observe baseline** — capture state relevant to the skill.
|
|
2. **make prediction** — predict outcome; log it.
|
|
3. **act** — execute actions (possibly calling `read_next()`-style SemIf decisions for arguments).
|
|
4. **observe outcome** — capture post-action state.
|
|
5. **self-assess** (LLM) — "have I succeeded?" → success/failure + summary + **updated request** back to the priority queue.
|
|
|
|
- The self-assessment result **labels** every SemIf decision made during that run (`choice`, `score`, navigation, argument selection): the predicted distribution vs. the observed outcome is one labeled training row.
|
|
- Per-skill cost budget: a run that exceeds it fails fast and requeues with a degraded goal.
|
|
|
|
## Learning / fine-tuning pipeline
|
|
|
|
- **Log**: every decision row → `{state, question, options, predicted_probs, observed_outcome}` — SemIf `decisions.jsonl`-compatible, so logs replay directly into the scorer.
|
|
- **Cost function**: loss between the predicted distribution and the observed outcome — cross-entropy / NLL under a proper scoring rule, plus optional calibration loss. This is the training signal.
|
|
- **Labels**: self-assessment output by default; human confirmations/corrections override and become high-weight labels.
|
|
- **"Dreaming"** — a cronjob at a regular interval: accumulate logged rows → compute cost over the period → fine-tune the decision model on them → CI/CD validates (accuracy / ECE on a held-out slice, prompt-hash regression) → swap in the new pinned model revision.
|
|
- **GPU offload**: training runs on a beefier GPU; the running agent keeps a frozen inference revision until a swap validates.
|
|
- Cold start: no labels yet — seed from self-assessed runs only; human overrides accelerate early calibration.
|
|
|
|
## Worked examples
|
|
|
|
### Example 1
|
|
- current_process: none
|
|
- input: "send my girlfriend an email that says that I'm going to be late to the party"
|
|
- contains_request: yes
|
|
- interrupt_current: yes
|
|
- skill_selection: email => compose => transcribe(input): [code block that uses SemIf `read_next()` to determine "girlfriend" from contacts, plus a normally functioning LLM for the email body]
|
|
|
|
### Example 2
|
|
- current_process: driving car
|
|
- input: "send my girlfriend an email that says that I'm going to be late to the party"
|
|
- interrupt_current: no
|
|
- priority: .21
|
|
- skill_selection: response => rejection(input)
|
|
|
|
### Example 3
|
|
- current_process: driving car
|
|
- input: "holy shit stop!"
|
|
- contains_request: yes
|
|
- interrupt_current: yes
|
|
- skill_selection: driving => decelerate(input)
|
|
|
|
### Example 4 (skill creation)
|
|
- current_process: none
|
|
- input: "tell me if my package was delivered"
|
|
- contains_request: yes
|
|
- interrupt_current: yes
|
|
- skill_selection: tracking => (no leaf) => create_skill
|
|
- opencode authors `tracking.check_delivery` from the input + registry conventions → manifest registered → requeue → skill_selection: tracking => check_delivery(input)
|
|
|
|
### Example 5 (self-assessment + dream)
|
|
- current_process: email compose
|
|
- input: "send my girlfriend an email…" → email compose succeeds but sends to the wrong contact (`read_next` picked wrong)
|
|
- self-assess: fail → labels the `read_next` decision row (predicted_probs vs wrong outcome)
|
|
- cost computed, logged
|
|
- next dream cycle: row included in fine-tune → model revision swapped → `read_next` recalibrated
|
|
|
|
## Open questions
|
|
|
|
- **Safety / authority**: which inputs may interrupt high-stakes processes (driving-grade)? Is interrupt a per-skill permission, not a global default?
|
|
- **Queue persistence**: is the priority queue in-memory or durable across restarts?
|
|
- **Input taxonomy**: enumerate intake sources and their `contains_request` gating semantics.
|
|
- **Privacy boundary**: all data stays local — confirm no telemetry even for dreaming.
|
|
- **Override UX**: how do human confirmations/corrections get surfaced and captured cheaply? |