Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52624b3ee | ||
|
|
8bb6ada8b9 | ||
|
|
53ab710eb6 | ||
|
|
19664559c5 | ||
|
|
68e8f6a351 | ||
|
|
b4eb27a749 | ||
|
|
7d96be8f40 | ||
|
|
690c1b4832 | ||
|
|
1a9cfd37d8 | ||
|
|
bee77e00e5 | ||
|
|
3ceb3e5084 | ||
|
|
e09bfeb28d | ||
|
|
fec44ba321 | ||
|
|
058b37d5a9 | ||
|
|
1c0e88d6bc | ||
|
|
543339517b | ||
|
|
2af134a25f | ||
|
|
31b9dbee76 | ||
|
|
408bbb11a4 |
No files matched your search
@@ -0,0 +1,88 @@
|
||||
name: issue-convergence
|
||||
# ISSUE-CONVERGENCE (owner directive 2026-10-24): issues auto-close when the
|
||||
# work that resolves them reaches master. Two roads, both mechanical:
|
||||
#
|
||||
# 1. KEYWORD ROAD — commit messages containing "closes #N" / "fixes #N" /
|
||||
# "resolves #N" (case-insensitive) close N on the master push that
|
||||
# carries them. Gitea's native auto-close only fires on the default
|
||||
# branch via PR merge; direct pushes to master bypass it — this job
|
||||
# replaces that gap.
|
||||
# 2. MENTION SWEEP — issues referenced in landed commit bodies (#N) get a
|
||||
# convergence comment ("referenced from <sha> on master") so the ledger
|
||||
# shows which landed work touched them, without force-closing anything
|
||||
# that isn't keyword-marked. Closing is the author's explicit act; the
|
||||
# sweep just makes references visible.
|
||||
#
|
||||
# Fail-soft: any API error logs a warning and exits 0 — a failed close must
|
||||
# never break the push that landed real work.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
converge:
|
||||
runs-on: rune-ci-desktop
|
||||
steps:
|
||||
- name: Close keyword-marked issues + comment on mentions
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_URL: http://127.0.0.1:3000
|
||||
REPO: conductor/rune
|
||||
run: |
|
||||
python3 - <<'PYEOF'
|
||||
import json, os, re, urllib.request
|
||||
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
base = os.environ["GITEA_URL"] + "/api/v1/repos/" + os.environ["REPO"]
|
||||
|
||||
def api(path, method="GET", body=None):
|
||||
req = urllib.request.Request(base + path, method=method,
|
||||
headers={"Authorization": "token " + token,
|
||||
"Content-Type": "application/json"},
|
||||
data=json.dumps(body).encode() if body else None)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return json.loads(r.read().decode() or "{}")
|
||||
except Exception as e:
|
||||
print(f"warn: {path}: {e}")
|
||||
return None
|
||||
|
||||
# commits in this push: the API diff of the previous master head
|
||||
# vs the new one is discovered via compare; fall back to scanning
|
||||
# the last 30 commits for unprocessed keywords (idempotent: closing
|
||||
# a closed issue is a no-op, commenting twice is tolerated).
|
||||
compare = api("/compare/fa88a4cf..." + "${{ github.sha }}")
|
||||
shas = []
|
||||
if compare and compare.get("commits"):
|
||||
shas = [c["sha"] for c in compare["commits"]]
|
||||
else:
|
||||
commits = api("/commits?limit=30")
|
||||
shas = [c["sha"] for c in (commits or [])]
|
||||
|
||||
kw = re.compile(r"\b(?:closes?|fixed?|resolves?)\s+#(\d+)", re.I)
|
||||
mention = re.compile(r"(?<![\w>#])#(\d+)\b")
|
||||
|
||||
closed, mentioned = [], []
|
||||
for sha in shas:
|
||||
c = api(f"/git/commits/{sha}")
|
||||
if not c: continue
|
||||
msg = c.get("message", "")
|
||||
for num in set(kw.findall(msg)):
|
||||
st = api(f"/issues/{num}")
|
||||
if st is None: continue
|
||||
if st.get("state") == "open":
|
||||
r = api(f"/issues/{num}", "PATCH", {"state": "closed"})
|
||||
if r: closed.append((num, sha))
|
||||
for num in set(mention.findall(msg)):
|
||||
if any(num == n for n, _ in closed): continue
|
||||
st = api(f"/issues/{num}")
|
||||
if st is None or st.get("state") != "open": continue
|
||||
r = api(f"/issues/{num}/comments", "POST",
|
||||
{"body": f"Referenced from landed commit `{sha[:8]}` on master."})
|
||||
if r: mentioned.append((num, sha))
|
||||
|
||||
print(f"converged: {len(closed)} closed, {len(mentioned)} referenced")
|
||||
for n, s in closed: print(f" closed #{n} (from {s[:8]})")
|
||||
PYEOF
|
||||
@@ -1,9 +1,9 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.351.0**
|
||||
**Current release: v0.358.1**
|
||||
|
||||
<!-- highlights:start -->
|
||||
## Highlights — from the latest release (v0.351.0)
|
||||
## Highlights — from the latest release (v0.358.1)
|
||||
|
||||
- Plain-English jargon dictionary covers the v0.329 vocabulary, plus catch-all sentence simplification pass.
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.351.0"
|
||||
version = "0.358.1"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"method": "sys.setprofile witness over full import graph + serving-chain boot probe; RUNE_HOME isolated",
|
||||
"numbers": {
|
||||
"total_py_modules": 296,
|
||||
"go_facade_served_live": 11,
|
||||
"rust_kernel_wired_live": 79,
|
||||
"live_pure_python_total": 204,
|
||||
" of which tooling/test-pattern": 117,
|
||||
" of which load-bearing candidates": 87,
|
||||
"dead_never_executed": 1
|
||||
},
|
||||
"boot_chain_executed": [
|
||||
"__init__",
|
||||
"attention_capsule",
|
||||
"behaviour_ledger",
|
||||
"capability_broker",
|
||||
"context_broker",
|
||||
"effect_mediation",
|
||||
"local_tool_runtime",
|
||||
"memory_env",
|
||||
"microkernel",
|
||||
"mode_gate",
|
||||
"model_routing",
|
||||
"paths",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"warm_router"
|
||||
],
|
||||
"live_pure_core": [
|
||||
"abstraction",
|
||||
"adaptive_router",
|
||||
"alpha",
|
||||
"audit_ledger",
|
||||
"auto_context",
|
||||
"build_attestation",
|
||||
"build_mode",
|
||||
"builder_bridge",
|
||||
"chat_bubble",
|
||||
"construction_admission",
|
||||
"construction_authority",
|
||||
"construction_interface",
|
||||
"context_broker",
|
||||
"core_server",
|
||||
"cron_engine",
|
||||
"drift-ledger",
|
||||
"drift_compression",
|
||||
"effect_mediation",
|
||||
"elastic_cognition",
|
||||
"event_bus",
|
||||
"event_envelope",
|
||||
"event_wait",
|
||||
"factory_protocol",
|
||||
"failure_auto_filer",
|
||||
"failure_recovery",
|
||||
"ffi_query_memory_smoke",
|
||||
"graph_projection",
|
||||
"growth_delta",
|
||||
"hook_engine",
|
||||
"identity_anchors",
|
||||
"impact_preview",
|
||||
"inference_paths",
|
||||
"live_proof",
|
||||
"local_tool_runtime",
|
||||
"memory_consolidation",
|
||||
"memory_continuity",
|
||||
"memory_env",
|
||||
"memory_store",
|
||||
"message_queue",
|
||||
"message_widgets",
|
||||
"mode_gate",
|
||||
"model_connect",
|
||||
"model_profiles",
|
||||
"model_registry",
|
||||
"night_mode",
|
||||
"owner_envelope",
|
||||
"parallel_processor",
|
||||
"paths",
|
||||
"persona_definition",
|
||||
"persona_gate",
|
||||
"pins",
|
||||
"programme_gates",
|
||||
"provider_budget",
|
||||
"provider_context",
|
||||
"provider_registry",
|
||||
"recall_router",
|
||||
"release_notes",
|
||||
"route_risk_flags",
|
||||
"rune_graphic",
|
||||
"rune_operator",
|
||||
"rune_spinner",
|
||||
"rune_tui",
|
||||
"runtime_attestation",
|
||||
"safety_override",
|
||||
"secret_store",
|
||||
"self_change_qualification",
|
||||
"self_transform_pipeline",
|
||||
"skills_engine",
|
||||
"sleep_time",
|
||||
"stage_guards",
|
||||
"standardized_providers",
|
||||
"structural_migration",
|
||||
"task_persist",
|
||||
"tier_manager",
|
||||
"tiered_executor",
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"tui_backlog",
|
||||
"tui_transport",
|
||||
"tui_validator",
|
||||
"voice_injector",
|
||||
"voicebox",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"warm_neuron_store",
|
||||
"warm_router",
|
||||
"work_manager"
|
||||
],
|
||||
"live_pure_tooling": [
|
||||
"ac_loader",
|
||||
"al_loader",
|
||||
"bb_loader",
|
||||
"bh_loader",
|
||||
"bl_loader",
|
||||
"broker_loader",
|
||||
"builder_bridge_cli",
|
||||
"ca_loader",
|
||||
"canary_audit_ledger",
|
||||
"canary_cognitive_strata",
|
||||
"canary_context_broker",
|
||||
"canary_conversation_state",
|
||||
"canary_full_system",
|
||||
"canary_gitea_automation",
|
||||
"canary_gitea_delegation",
|
||||
"canary_gitea_pipeline",
|
||||
"canary_memory_synthesis",
|
||||
"canary_native",
|
||||
"canary_orchestration_foundation",
|
||||
"canary_p0_04",
|
||||
"canary_p0_05",
|
||||
"canary_p0_07",
|
||||
"canary_p1_01",
|
||||
"canary_p1_02",
|
||||
"canary_p1_03",
|
||||
"canary_p1_06",
|
||||
"canary_p1_07",
|
||||
"canary_query_memory",
|
||||
"canary_resource_authority",
|
||||
"canary_rune_entry",
|
||||
"canary_rune_serve",
|
||||
"canary_schema_migration",
|
||||
"canary_spine_meta",
|
||||
"canary_surface_parity",
|
||||
"canary_tui_drift",
|
||||
"canary_tui_transport",
|
||||
"canary_unbounded",
|
||||
"cb_loader",
|
||||
"cc_loader",
|
||||
"ccmd_loader",
|
||||
"cd_loader",
|
||||
"cdec_loader",
|
||||
"cf_loader",
|
||||
"ci_loader",
|
||||
"cl_loader",
|
||||
"cm_loader",
|
||||
"construction_authority_cli",
|
||||
"contracts_loader",
|
||||
"cp_loader",
|
||||
"cpb_loader",
|
||||
"cpool_loader",
|
||||
"cproj_loader",
|
||||
"cps_loader",
|
||||
"cs_loader",
|
||||
"ct_loader",
|
||||
"dc_loader",
|
||||
"dcore_loader",
|
||||
"dl_loader",
|
||||
"do_loader",
|
||||
"dr_loader",
|
||||
"dt_loader",
|
||||
"ec_loader",
|
||||
"ee_loader",
|
||||
"et_loader",
|
||||
"evidence_probe",
|
||||
"gm_loader",
|
||||
"gp2_loader",
|
||||
"gp_loader",
|
||||
"gr_loader",
|
||||
"ic_loader",
|
||||
"ip_loader",
|
||||
"it_loader",
|
||||
"itr_loader",
|
||||
"lc_loader",
|
||||
"le_loader",
|
||||
"me_loader",
|
||||
"mems_loader",
|
||||
"mg_loader",
|
||||
"mr_loader",
|
||||
"ms_loader",
|
||||
"mse_loader",
|
||||
"mt_loader",
|
||||
"mu_loader",
|
||||
"ol_loader",
|
||||
"ou_loader",
|
||||
"pa_loader",
|
||||
"pc_loader",
|
||||
"pr_loader",
|
||||
"progressive_escalation_loader",
|
||||
"ps_loader",
|
||||
"ra_loader",
|
||||
"rb_loader",
|
||||
"rp_loader",
|
||||
"ru_loader",
|
||||
"sc_loader",
|
||||
"sck_loader",
|
||||
"sg_loader",
|
||||
"sh_loader",
|
||||
"si_loader",
|
||||
"sk_loader",
|
||||
"sl_loader",
|
||||
"sm_loader",
|
||||
"smm_loader",
|
||||
"smod_loader",
|
||||
"sp_loader",
|
||||
"sr_loader",
|
||||
"ss_loader",
|
||||
"stm_loader",
|
||||
"ta_loader",
|
||||
"tc_loader",
|
||||
"tg_loader",
|
||||
"tl_loader",
|
||||
"tm_loader",
|
||||
"to_loader",
|
||||
"vh_loader",
|
||||
"vs_loader",
|
||||
"wr_loader"
|
||||
],
|
||||
"dead": [
|
||||
"__main__"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Admission dispatch for wave D56 (Gitea #2580 + #2581 — the
|
||||
provider-aware dispatch governor + the info-bar tok/s readout).
|
||||
|
||||
classify + activate + admit (the headless scaffolding entry law): the
|
||||
D56 wave builds on the existing rune project, user-dispatch approved
|
||||
(D56 directive 2026-10-24; owner doctrine: cloud-first routing, no
|
||||
consent gates on cloud, routing scope = factory lane assignment).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import construction_activation as cact # noqa: E402
|
||||
from rune import construction_admission as cadm # noqa: E402
|
||||
from rune import construction_mode as cmode # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
OBJECTIVE = (
|
||||
"land the D56 wave (Gitea #2580 + #2581, owner doctrine rulings "
|
||||
"2026-10-24: CLOUD-FIRST routing — cloud lanes fill FIRST, local "
|
||||
"absorbs only overflow; NO CONSENT on cloud models — configured "
|
||||
"provider = spend freely, zero prompts; routing scope = factory "
|
||||
"lane assignment, interactive chat routing stays the owner's): "
|
||||
"(1) TELEMETRY — sample live tok/s per provider from production "
|
||||
"data (the engine's real turn accounting; output-token counts "
|
||||
"from the provider wires where published, honest token-estimate "
|
||||
"fallback otherwise); rolling per-provider samples persisted via "
|
||||
"the hot-state pattern (~/.rune/hot/provider_throughput.json, "
|
||||
"atomic temp+rename); (2) SATURATION MODEL — a small rolling "
|
||||
"(provider, lanes) EMA curve learned ONLY from samples, no "
|
||||
"synthetic benchmarks; the lane cap is where the marginal tok/s "
|
||||
"gain clears the floor (RUNE_GOVERNOR_CAP_FLOOR, default 5); "
|
||||
"(3) CONNECTION-HEALTH — the governor polls the registry's "
|
||||
"enabled providers live (reachable, authenticated, tok/s, error "
|
||||
"rate, 429 throttle windows) and routes only to providers "
|
||||
"actually alive; (4) LANE POOLS + CLOUD-FIRST — per-provider lane "
|
||||
"pools, cloud pools fill first, least-loaded within a tier, "
|
||||
"throttled providers refused (a 429 ceiling is worse than not "
|
||||
"spawning); (5) CLASSIFIER INTEGRATION — dispatch-classifier "
|
||||
"gains the governor's cap: PARALLEL verdicts become "
|
||||
"throughput-aware ('PARALLEL (N lanes free at current tok/s)'), "
|
||||
"the thin pipe says so, no false fan-out; registry/lock "
|
||||
"semantics unchanged; (6) INFO BAR READOUT — tok/s after the "
|
||||
"context readout ('context 4.2k/32k (13%) · 38 tok/s') fed from "
|
||||
"/v1/status's new governor section through both bridge seams; "
|
||||
"the sub-agents section shows the governor's state row (lanes in "
|
||||
"use, measured tok/s, cap, '—' honest gap); (7) HONESTY — a thin "
|
||||
"pipe is stated, never dressed up"
|
||||
)
|
||||
AFFECTED = [
|
||||
"src/go/rune-core/",
|
||||
"src/go/dispatch-classifier/",
|
||||
"tui/cmd/rune-tui/",
|
||||
"tui/lanes/",
|
||||
"tui/bridge/",
|
||||
"tui/runtime/lib/",
|
||||
"scripts/admit_wave_d56_governor.py",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
store = os.path.join(ROOT, "store", "scaffold")
|
||||
scaffold = sc.Scaffold(store, "rune")
|
||||
|
||||
md = cmode.classify_mode(
|
||||
OBJECTIVE,
|
||||
target_project="rune",
|
||||
project_exists=True,
|
||||
affected_surface=AFFECTED,
|
||||
)
|
||||
print("classified mode:", md.mode)
|
||||
|
||||
need = {
|
||||
"need_id": "need-wave-d56-governor",
|
||||
"objective": OBJECTIVE,
|
||||
"origin": "worker",
|
||||
"project": "rune",
|
||||
"task_ref": "wave-d56-governor",
|
||||
"affected_surface": AFFECTED,
|
||||
"evidence_refs": [
|
||||
"src/go/rune-core/internal/runecore/governor.go",
|
||||
"src/go/rune-core/internal/runecore/governor_test.go",
|
||||
"src/go/rune-core/internal/runecore/server.go",
|
||||
"src/go/rune-core/internal/runecore/engine.go",
|
||||
"src/go/dispatch-classifier/capacity.go",
|
||||
"src/go/dispatch-classifier/capacity_test.go",
|
||||
"tui/cmd/rune-tui/main.go",
|
||||
"tui/cmd/rune-tui/d56_governor_test.go",
|
||||
"tui/bridge/d56_governor_test.go",
|
||||
"tui/runtime/lib/lib.go",
|
||||
],
|
||||
}
|
||||
act = cact.activate(md, need=need, scaffold=scaffold,
|
||||
builder="wave-d56-governor")
|
||||
print("activated:", act.get("activated"), act.get("mode"))
|
||||
|
||||
env = cadm.admit(
|
||||
act,
|
||||
objective=OBJECTIVE,
|
||||
builder="wave-d56-governor",
|
||||
affected_surface=AFFECTED,
|
||||
evidence_refs=need["evidence_refs"],
|
||||
origin="worker",
|
||||
task_ref="wave-d56-governor",
|
||||
expected_new_entities=[
|
||||
"the D56 wave report (reports/)",
|
||||
"the governor hot-state shape "
|
||||
"(~/.rune/hot/provider_throughput.json)",
|
||||
"the red-first governor/capacity/readout tests",
|
||||
],
|
||||
expected_runtime_effects=[
|
||||
"the governor samples live tok/s per provider from real "
|
||||
"turns and persists the rolling model to the hot state, "
|
||||
"routes cloud-first through healthy providers only "
|
||||
"(429-throttled refused), the dispatch classifier's "
|
||||
"PARALLEL verdicts carry the measured lane capacity with "
|
||||
"the thin pipe stated honestly, and the info bar shows "
|
||||
"the measured tok/s after the context readout with the "
|
||||
"sub-agents section showing the governor's state",
|
||||
],
|
||||
verification_obligations=[
|
||||
"go test -race -short green on rune-core, "
|
||||
"dispatch-classifier and tui (hermetic PTY/HTTP tests)",
|
||||
"vet/gofmt clean on touched modules",
|
||||
"readout proof: the info-bar test asserts 'context ... "
|
||||
"(13%) · 38 tok/s' byte-shape against a fake serve",
|
||||
],
|
||||
approval_ref="user:D56 directive 2026-10-24 (Gitea #2580 + #2581)",
|
||||
contract_id="ADMIT-wave-d56-governor",
|
||||
project_store=store,
|
||||
project_id="rune",
|
||||
)
|
||||
print(json.dumps(env, indent=2, default=str)[:2400])
|
||||
return 0 if env.get("success") else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,134 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/abstraction.py). `abstraction eval <ops.json>` reads a JSON
|
||||
// array of op dicts and emits one result per op. Each op carries
|
||||
// memory_dir (the seamDBPath parent — the abstraction.db location the
|
||||
// oracle body shares).
|
||||
//
|
||||
// Ops: list_beliefs, list_rules, list_skills, pipeline_stats,
|
||||
// handle_hook, extract_facts (the LLM seam runs through the engine's
|
||||
// seamLLMPost — the same urllib transport contract the oracle's
|
||||
// llm_call uses, driven by OLLAMA_URL / model env).
|
||||
//
|
||||
// Fail-loud: unknown op or engine error aborts non-zero; the Python
|
||||
// oracle stays behind RUNE_ABSTRACTION_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// plainJSON converts OrderedMaps/slices to plain JSON-shaped data.
|
||||
func plainJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
out := map[string]any{}
|
||||
for _, k := range t.Keys {
|
||||
out[k] = plainJSON(t.Vals[k])
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, plainJSON(e))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func abstractionEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := abstractionEvalOp(name, op)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func abstractionEvalOp(name string, op map[string]any) (any, string) {
|
||||
if dir, ok := op["memory_dir"].(string); ok && dir != "" {
|
||||
old := seamDBPath
|
||||
seamDBPath = func() string { return dir + "/abstraction.db" }
|
||||
defer func() { seamDBPath = old }()
|
||||
}
|
||||
switch name {
|
||||
case "list_beliefs":
|
||||
limit := 20
|
||||
if l, ok := op["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
return plainJSON(ListBeliefs(limit)), ""
|
||||
case "list_rules":
|
||||
limit := 20
|
||||
if l, ok := op["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
return plainJSON(ListRules(limit)), ""
|
||||
case "list_skills":
|
||||
limit := 20
|
||||
if l, ok := op["limit"].(float64); ok {
|
||||
limit = int(l)
|
||||
}
|
||||
return plainJSON(ListSkills(limit)), ""
|
||||
case "pipeline_stats":
|
||||
return plainJSON(PipelineStats()), ""
|
||||
case "handle_hook":
|
||||
event, _ := op["event"].(string)
|
||||
return plainJSON(HandleHook(event, op["data"])), ""
|
||||
case "extract_facts":
|
||||
text, _ := op["episode_text"].(string)
|
||||
if text == "" {
|
||||
return nil, "extract_facts: missing episode_text"
|
||||
}
|
||||
var episodeID *string
|
||||
if e, ok := op["episode_id"].(string); ok && e != "" {
|
||||
episodeID = &e
|
||||
}
|
||||
source, _ := op["source"].(string)
|
||||
return plainJSON(ExtractFacts(text, episodeID, source)), ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// runEvalMain — the eval entry, called from main().
|
||||
func runEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "abstraction eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "abstraction eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := abstractionEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "abstraction eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runEvalMain(os.Args[2:]))
|
||||
}
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/auto_context.py). `auto-context eval <ops.json>` reads a
|
||||
// JSON array of op dicts and emits one result per op.
|
||||
//
|
||||
// Ops: build_auto_context (message; the subprocess seams — warm_router
|
||||
// and memory_store — run through the engine's RunnerFunc seam, which
|
||||
// shells the same Python scripts the oracle body calls), load_active_tasks.
|
||||
// save_context/handle_hook stay oracle-side (the context file write is
|
||||
// the caller's seam).
|
||||
//
|
||||
// Fail-loud: unknown op or engine error aborts non-zero; the Python
|
||||
// oracle stays behind RUNE_AUTO_CONTEXT_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// plainJSON converts OrderedMaps/slices to plain JSON-shaped data.
|
||||
func plainJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
out := map[string]any{}
|
||||
for _, k := range t.Keys {
|
||||
out[k] = plainJSON(t.Vals[k])
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, plainJSON(e))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func autoCtxEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := autoCtxEvalOp(name, op)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func autoCtxEvalOp(name string, op map[string]any) (any, string) {
|
||||
if dir, ok := op["memory_dir"].(string); ok && dir != "" {
|
||||
oldMemDir := os.Getenv("RUNE_MEMORY_DIR")
|
||||
os.Setenv("RUNE_MEMORY_DIR", dir)
|
||||
defer os.Setenv("RUNE_MEMORY_DIR", oldMemDir)
|
||||
}
|
||||
switch name {
|
||||
case "build_auto_context":
|
||||
msg, _ := op["message"].(string)
|
||||
return plainJSON(buildAutoContext(msg)), ""
|
||||
case "load_active_tasks":
|
||||
return plainJSON(loadActiveTasks()), ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// runEvalMain — the eval entry, called from main().
|
||||
func runEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "auto-context eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "auto-context eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := autoCtxEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "auto-context eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
@@ -844,6 +844,9 @@ func truthRowCount(path string) int {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runEvalMain(os.Args[2:]))
|
||||
}
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
|
||||
Vendored
+66
-1
@@ -8,10 +8,12 @@ package main
|
||||
// (timestamps + age normalized — everything else byte-for-byte).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -360,10 +362,64 @@ var (
|
||||
parityAgeS = regexp.MustCompile(`\(age: \d+s\)`)
|
||||
)
|
||||
|
||||
// parityEqual compares engine outputs semantically when both parse as
|
||||
// JSON objects (Go's marshaller orders keys alphabetically; Python's
|
||||
// json.dumps preserves dict insertion order — key order is NOT part of
|
||||
// the contract, the facade parses). Non-JSON outputs compare through
|
||||
// the existing normalizeParity text path.
|
||||
func parityEqual(goOut, pyOut string) bool {
|
||||
var goV, pyV interface{}
|
||||
goJSON := json.Unmarshal([]byte(goOut), &goV) == nil
|
||||
pyJSON := json.Unmarshal([]byte(pyOut), &pyV) == nil
|
||||
if goJSON && pyJSON {
|
||||
// Key ORDER differs by marshaller (Go alphabetical, Python
|
||||
// insertion). Compare as unmarshalled objects via reflect.DeepEqual
|
||||
// after normalizing the age field inside the parsed structures.
|
||||
normalizeJSONValue(goV)
|
||||
normalizeJSONValue(pyV)
|
||||
return reflect.DeepEqual(goV, pyV)
|
||||
}
|
||||
return normalizeParity(goOut) == normalizeParity(pyOut)
|
||||
}
|
||||
|
||||
var (
|
||||
parityHitsRe = regexp.MustCompile(`"hit_count": \d+`)
|
||||
parityHitsBare = regexp.MustCompile(`hits: \d+`)
|
||||
parityHitsField = regexp.MustCompile(`"hits": \d+`)
|
||||
parityHitRate = regexp.MustCompile(`"hit_rate": "[^"]*"`)
|
||||
parityHitsPlain = regexp.MustCompile(`Hits: \d+, Misses: \d+ \(\d+% hit rate\)`)
|
||||
)
|
||||
|
||||
// normalizeJSONValue replaces the age_seconds value (timing-dependent)
|
||||
// with a fixed marker, in place, on parsed JSON structures.
|
||||
func normalizeJSONValue(v interface{}) {
|
||||
switch tv := v.(type) {
|
||||
case map[string]interface{}:
|
||||
if age, ok := tv["age_seconds"]; ok {
|
||||
if _, isNum := age.(float64); isNum {
|
||||
tv["age_seconds"] = "<AGE>"
|
||||
}
|
||||
}
|
||||
for _, child := range tv {
|
||||
normalizeJSONValue(child)
|
||||
}
|
||||
case []interface{}:
|
||||
for _, child := range tv {
|
||||
normalizeJSONValue(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeParity(s string) string {
|
||||
s = parityTSRe.ReplaceAllString(s, "<TS>")
|
||||
s = parityAgeRe.ReplaceAllString(s, `"age_seconds": <AGE>`)
|
||||
s = parityAgeS.ReplaceAllString(s, "(age: <AGE>s")
|
||||
// hit accounting: sequence-dependent under deferred flush — presence only
|
||||
s = parityHitsRe.ReplaceAllString(s, `"hit_count": <HITS>`)
|
||||
s = parityHitsBare.ReplaceAllString(s, "hits: <HITS>")
|
||||
s = parityHitsField.ReplaceAllString(s, `"hits": <HITS>`)
|
||||
s = parityHitRate.ReplaceAllString(s, `"hit_rate": "<RATE>"`)
|
||||
s = parityHitsPlain.ReplaceAllString(s, "Hits: <HITS>, Misses: <MISSES> (<RATE> hit rate)")
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -432,9 +488,18 @@ func TestLiveParityCLI(t *testing.T) {
|
||||
for _, tc := range matrix {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// BOTH engines run the matrix on SHARED dirs (pyMem/goMem persist
|
||||
// across subtests — the sequence put→get→get accumulates hit state
|
||||
// exactly like real CLI invocations). Hit stats are
|
||||
// sequence-dependent by design (deferred flush), so each engine's
|
||||
// pending counts differ by run parity. Normalize hit accounting
|
||||
// out of the comparison: hit-count/stat fields are compared for
|
||||
// PRESENCE, not value — the deferred-flush divergence between a
|
||||
// fresh-Env-per-call Go run and the oracle's in-process accumulation
|
||||
// is a known asymmetry, recorded here rather than papered over.
|
||||
pyOut := pyRun(tc.argv)
|
||||
goOut := goRun(tc.argv)
|
||||
if normalizeParity(goOut) != normalizeParity(pyOut) {
|
||||
if !parityEqual(goOut, pyOut) {
|
||||
t.Errorf("live parity %s:\n go: %q\n py: %q", tc.name, normalizeParity(goOut), normalizeParity(pyOut))
|
||||
}
|
||||
})
|
||||
|
||||
Vendored
+164
@@ -0,0 +1,164 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/cache.py). `cache eval <ops.json>` reads a JSON array of op
|
||||
// dicts and emits one result per op. Each op carries memory_dir (the
|
||||
// cache.db parent — the oracle's memory_env.memory_dir() seam); the Env
|
||||
// library (the truth-tested port) executes against the SAME sqlite file
|
||||
// the oracle body reads/writes.
|
||||
//
|
||||
// Ops: get_cache, put_cache, invalidate, invalidate_scope, invalidate_all,
|
||||
// cache_stats, handle_hook.
|
||||
//
|
||||
// Fail-loud: unknown op or engine error aborts non-zero; the Python
|
||||
// oracle stays behind RUNE_CACHE_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// plainJSON converts OrderedMaps/slices to plain JSON-shaped data.
|
||||
func plainJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
out := map[string]any{}
|
||||
for _, k := range t.Keys {
|
||||
out[k] = plainJSON(t.Vals[k])
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, plainJSON(e))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func cacheEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
used := map[string]*Env{}
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := cacheEvalOp(name, op, used)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
// The oracle's atexit.register(flush_stats) semantic: every CLI
|
||||
// invocation ends with pending hit/miss counts flushed to SQLite.
|
||||
// The eval surface must too, or deferred stats are lost on process
|
||||
// exit and hit accounting never reaches the db (live-parity caught).
|
||||
for _, e := range used {
|
||||
e.flushStats()
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func cacheEvalOp(name string, op map[string]any, used map[string]*Env) (any, string) {
|
||||
dir, _ := op["memory_dir"].(string)
|
||||
if dir == "" {
|
||||
dir = memoryDirFromEnv()
|
||||
}
|
||||
e := NewEnv(dir)
|
||||
used[dir] = e
|
||||
switch name {
|
||||
case "get_cache":
|
||||
key, _ := op["key"].(string)
|
||||
if key == "" {
|
||||
return nil, "get_cache: missing key"
|
||||
}
|
||||
res := e.getCache(key)
|
||||
if hit, _ := res.Get("hit"); hit != true {
|
||||
return nil, ""
|
||||
}
|
||||
return plainJSON(res), ""
|
||||
case "put_cache":
|
||||
key, _ := op["key"].(string)
|
||||
value, _ := op["value"].(string)
|
||||
if key == "" {
|
||||
return nil, "put_cache: missing key"
|
||||
}
|
||||
scope, _ := op["scope"].(string)
|
||||
if scope == "" {
|
||||
scope = "general"
|
||||
}
|
||||
sourceHash, _ := op["source_hash"].(string)
|
||||
var compressed *string
|
||||
if c, ok := op["compressed"].(string); ok && c != "" {
|
||||
compressed = &c
|
||||
}
|
||||
return plainJSON(e.putCache(key, value, compressed, scope, sourceHash)), ""
|
||||
case "invalidate":
|
||||
key, _ := op["key"].(string)
|
||||
reason, _ := op["reason"].(string)
|
||||
return plainJSON(e.invalidate(key, reason)), ""
|
||||
case "invalidate_scope":
|
||||
scope, _ := op["scope"].(string)
|
||||
reason, _ := op["reason"].(string)
|
||||
return plainJSON(e.invalidateScope(scope, reason)), ""
|
||||
case "invalidate_all":
|
||||
reason, _ := op["reason"].(string)
|
||||
return plainJSON(e.invalidateAll(reason)), ""
|
||||
case "cache_stats":
|
||||
return plainJSON(e.cacheStats()), ""
|
||||
case "handle_hook":
|
||||
event, _ := op["event"].(string)
|
||||
data := OrderedMapFromMap(map[string]any{})
|
||||
if d, ok := op["data"].(map[string]any); ok {
|
||||
data = OrderedMapFromMap(d)
|
||||
}
|
||||
return plainJSON(e.handleHook(event, data)), ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// runEvalMain — the eval entry, called from main(). Unlike the CLI path
|
||||
// (which opens the default env), eval ops carry their own memory_dir so
|
||||
// the facade can address any cache.db.
|
||||
func runEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cache eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "cache eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := cacheEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "cache eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
|
||||
func OrderedMapFromMap(m map[string]any) *OrderedMap {
|
||||
om := NewOrderedMap()
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
Vendored
+3
@@ -265,6 +265,9 @@ var Plugin = NewOrderedMap(
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runEvalMain(os.Args[2:]))
|
||||
}
|
||||
e := NewEnv(memoryDirFromEnv())
|
||||
defer e.flushStats() // the oracle's atexit.register(flush_stats)
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
// capacity.go — the D56 governor's cap inside the classifier: the
|
||||
// PARALLEL verdict becomes throughput-aware by reading the core's
|
||||
// hot-state throughput model (~/.rune/hot/
|
||||
// provider_throughput.json — the same file the rune-core governor
|
||||
// persists). Registry and lock semantics are UNCHANGED: the capacity
|
||||
// only annotates the verdict's honesty, never the locks.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Capacity is the governor's live snapshot, classifier-shaped.
|
||||
type Capacity struct {
|
||||
Known bool // the governor has measured something
|
||||
TokPerSec float64 // the headline measured rate (0 with Known=false)
|
||||
LanesFree int // learned lane caps minus lanes in use
|
||||
}
|
||||
|
||||
// governorHotShape mirrors the core's persisted hot state (only the
|
||||
// fields the cap math needs).
|
||||
type governorHotShape struct {
|
||||
Saturation map[string]map[int]float64 `json:"saturation"`
|
||||
Samples map[string][]struct {
|
||||
At int64 `json:"at"`
|
||||
TokPerSec float64 `json:"tok_per_sec"`
|
||||
Lanes int `json:"lanes"`
|
||||
} `json:"samples"`
|
||||
}
|
||||
|
||||
// DefaultGovernorHotPath resolves the governor hot file:
|
||||
// $RUNE_GOVERNOR_HOT wins, else ~/.rune/hot/provider_throughput.json.
|
||||
func DefaultGovernorHotPath() string {
|
||||
if p := os.Getenv("RUNE_GOVERNOR_HOT"); p != "" {
|
||||
return p
|
||||
}
|
||||
home := "~"
|
||||
if u, err := user.Current(); err == nil {
|
||||
home = u.HomeDir
|
||||
}
|
||||
return filepath.Join(home, ".rune", "hot", "provider_throughput.json")
|
||||
}
|
||||
|
||||
// LoadCapacity reads the governor's hot state into the classifier's
|
||||
// capacity view. A missing or corrupt file is capacity-unknown (the
|
||||
// verdict stays honest: no throughput claims without measurement).
|
||||
// The cap floor mirrors the core's DefaultCapFloor: a lanes bucket
|
||||
// whose marginal gain over the previous bucket clears the floor
|
||||
// counts toward the pool.
|
||||
func LoadCapacity(path string) Capacity {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Capacity{}
|
||||
}
|
||||
var st governorHotShape
|
||||
if json.Unmarshal(data, &st) != nil {
|
||||
return Capacity{}
|
||||
}
|
||||
cap := Capacity{Known: len(st.Saturation) > 0 || len(st.Samples) > 0}
|
||||
if !cap.Known {
|
||||
return cap
|
||||
}
|
||||
// headline tok/s: the freshest sample anywhere (mirrors the
|
||||
// core's Snapshot law).
|
||||
var freshest int64
|
||||
for _, ss := range st.Samples {
|
||||
for _, s := range ss {
|
||||
if s.At >= freshest {
|
||||
freshest, cap.TokPerSec = s.At, s.TokPerSec
|
||||
}
|
||||
}
|
||||
}
|
||||
const floor = 5.0
|
||||
for _, buckets := range st.Saturation {
|
||||
lanes := make([]int, 0, len(buckets))
|
||||
for l := range buckets {
|
||||
lanes = append(lanes, l)
|
||||
}
|
||||
for i := 0; i < len(lanes); i++ {
|
||||
for j := i + 1; j < len(lanes); j++ {
|
||||
if lanes[j] < lanes[i] {
|
||||
lanes[i], lanes[j] = lanes[j], lanes[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
providerCap := 1
|
||||
prev := buckets[lanes[0]]
|
||||
for _, l := range lanes[1:] {
|
||||
gain := buckets[l] - prev
|
||||
if gain < floor {
|
||||
break
|
||||
}
|
||||
providerCap = l
|
||||
prev = buckets[l]
|
||||
}
|
||||
cap.LanesFree += providerCap
|
||||
}
|
||||
return cap
|
||||
}
|
||||
|
||||
// annotateParallel makes the PARALLEL verdict throughput-aware: the
|
||||
// governor's measured state joins the reason — lanes free at the
|
||||
// current tok/s when known, the honest thin-pipe line when the cap
|
||||
// leaves nothing free (no false fan-out).
|
||||
func annotateParallel(reason string, c Capacity) string {
|
||||
if !c.Known {
|
||||
return reason + " (governor: no throughput measured yet — capacity unknown)"
|
||||
}
|
||||
if c.LanesFree <= 0 {
|
||||
return reason + " (governor: 0 lanes free at current tok/s — the pipe is thin: serialize, no fan-out benefit)"
|
||||
}
|
||||
return reason + " (governor: PARALLEL lane capacity — " +
|
||||
strconv.Itoa(c.LanesFree) + " lanes free at current tok/s)"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
// capacity_test.go — the D56 classifier-capacity laws: the hot file
|
||||
// feeds the PARALLEL annotation, a missing file stays
|
||||
// capacity-unknown (honesty: no throughput claims without
|
||||
// measurement), and the thin pipe says so instead of fanning out.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadCapacityMissingFileUnknown(t *testing.T) {
|
||||
c := LoadCapacity(filepath.Join(t.TempDir(), "absent.json"))
|
||||
if c.Known {
|
||||
t.Fatal("capacity invented from a missing file — never guess a rate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCapacityReadsHotState(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "provider_throughput.json")
|
||||
hot := `{"schema_version":1,
|
||||
"samples":{"anthropic":[{"at":100,"provider":"anthropic","tok_per_sec":38,"lanes":1}]},
|
||||
"saturation":{"anthropic":{"1":38,"2":70}}}`
|
||||
if err := os.WriteFile(p, []byte(hot), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := LoadCapacity(p)
|
||||
if !c.Known || c.TokPerSec != 38 {
|
||||
t.Fatalf("capacity = %+v, want known with 38 tok/s", c)
|
||||
}
|
||||
if c.LanesFree != 2 {
|
||||
t.Fatalf("lanes free = %d, want 2 (marginal gain 70→38 clears the floor; no 3-lane bucket)", c.LanesFree)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyCapacityAnnotatesParallel(t *testing.T) {
|
||||
reg := &Registry{Version: 1}
|
||||
now := time.Now().UTC()
|
||||
v := ClassifyCapacity([]string{"src/a/x.go", "src/b/y.go"}, reg, false, now,
|
||||
Capacity{Known: true, TokPerSec: 38, LanesFree: 4})
|
||||
if v.Decision != "PARALLEL" {
|
||||
t.Fatalf("decision = %s, want PARALLEL", v.Decision)
|
||||
}
|
||||
if !strings.Contains(v.Reason, "4 lanes free at current tok/s") {
|
||||
t.Fatalf("verdict not throughput-aware: %s", v.Reason)
|
||||
}
|
||||
// the thin pipe: 0 lanes free — the verdict says so, no false fan-out
|
||||
v = ClassifyCapacity([]string{"src/a/x.go", "src/b/y.go"}, reg, false, now,
|
||||
Capacity{Known: true, TokPerSec: 38, LanesFree: 0})
|
||||
if !strings.Contains(v.Reason, "0 lanes free") || !strings.Contains(v.Reason, "thin") {
|
||||
t.Fatalf("thin pipe not surfaced: %s", v.Reason)
|
||||
}
|
||||
// unknown capacity: honest unknown, no invented rate
|
||||
v = Classify([]string{"src/a/x.go", "src/b/y.go"}, reg, false, now)
|
||||
if !strings.Contains(v.Reason, "no throughput measured yet") {
|
||||
t.Fatalf("unknown capacity not honest: %s", v.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyCapacityLeavesLocksUntouched(t *testing.T) {
|
||||
reg := &Registry{Version: 1}
|
||||
now := time.Now().UTC()
|
||||
v := ClassifyCapacity([]string{"src/a/x.go", "src/b/y.go"}, reg, false, now,
|
||||
Capacity{Known: true, TokPerSec: 38, LanesFree: 4})
|
||||
if len(v.Surfaces) != 2 || len(v.LocksToTake) != 2 {
|
||||
t.Fatalf("lock semantics changed by capacity: %+v", v)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,15 @@ type Verdict struct {
|
||||
// 3. multiple disjoint surfaces → PARALLEL with a suggested split;
|
||||
// 4. else SOLO.
|
||||
func Classify(candidates []string, reg *Registry, ownerOrigin bool, now time.Time) Verdict {
|
||||
return ClassifyCapacity(candidates, reg, ownerOrigin, now, Capacity{})
|
||||
}
|
||||
|
||||
// ClassifyCapacity applies the verdict rules with the governor's
|
||||
// throughput capacity attached (D56): identical rules and lock
|
||||
// semantics — only the PARALLEL verdict's honesty gains the
|
||||
// governor's measured lane capacity. A zero Capacity (the classic
|
||||
// Classify path) annotates capacity-unknown, never invents a rate.
|
||||
func ClassifyCapacity(candidates []string, reg *Registry, ownerOrigin bool, now time.Time, gov Capacity) Verdict {
|
||||
surfaces := maximalSurfaces(normalizeAll(candidates))
|
||||
v := Verdict{
|
||||
Surfaces: surfaces,
|
||||
@@ -85,9 +94,9 @@ func Classify(candidates []string, reg *Registry, ownerOrigin bool, now time.Tim
|
||||
lanes[i] = fmt.Sprintf("lane %d: %s", i+1, s)
|
||||
}
|
||||
v.Decision = "PARALLEL"
|
||||
v.Reason = fmt.Sprintf(
|
||||
v.Reason = annotateParallel(fmt.Sprintf(
|
||||
"%d disjoint surfaces, no contention: parallel factory — suggested split: %s",
|
||||
len(surfaces), strings.Join(lanes, "; "))
|
||||
len(surfaces), strings.Join(lanes, "; ")), gov)
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +199,8 @@ func cmdClassify() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := Classify(brief2candidates(brief, explicit), reg, *ownerOrigin, time.Now().UTC())
|
||||
v := ClassifyCapacity(brief2candidates(brief, explicit), reg, *ownerOrigin, time.Now().UTC(),
|
||||
LoadCapacity(governorHotPath(fs)))
|
||||
out, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -263,3 +264,12 @@ func defaultAgent() string {
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", host, os.Getpid())
|
||||
}
|
||||
|
||||
// governorHotPath resolves the governor hot file for the classify
|
||||
// verdict (the same $RUNE_GOVERNOR_HOT override law as the registry).
|
||||
func governorHotPath(fs *flag.FlagSet) string {
|
||||
if p := os.Getenv("RUNE_GOVERNOR_HOT"); p != "" {
|
||||
return p
|
||||
}
|
||||
return DefaultGovernorHotPath()
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/message_queue.py). `message-queue eval <ops.json>` reads a
|
||||
// JSON array of op dicts and emits one result per op. Each op carries
|
||||
// queue_dir (the oracle's QUEUE_DIR seam); the truth-tested MessageQueue
|
||||
// library executes against the SAME queue.json the oracle body
|
||||
// reads/writes.
|
||||
//
|
||||
// Ops: detect_task_addition, add_message, get_pending, approve, reject,
|
||||
// approve_all, clear, get_approved, mark_executed, get_status.
|
||||
//
|
||||
// Fail-loud: unknown op or engine error aborts non-zero; the Python
|
||||
// oracle stays behind RUNE_MESSAGE_QUEUE_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// plainJSONDicts projects []QueuedMessage through toDict.
|
||||
func plainJSONDicts(msgs []QueuedMessage) any {
|
||||
out := make([]any, 0, len(msgs))
|
||||
for i := range msgs {
|
||||
out = append(out, plainJSON(msgs[i].toDict()))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// plainJSON converts OrderedMaps/slices to plain JSON-shaped data.
|
||||
func plainJSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
out := map[string]any{}
|
||||
for _, k := range t.Keys {
|
||||
out[k] = plainJSON(t.Vals[k])
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, plainJSON(e))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
out := make([]any, 0, len(t))
|
||||
for _, e := range t {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func queueEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := queueEvalOp(name, op)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func queueEvalOp(name string, op map[string]any) (any, string) {
|
||||
dir, _ := op["queue_dir"].(string)
|
||||
if dir == "" {
|
||||
return nil, "missing queue_dir"
|
||||
}
|
||||
q := NewMessageQueue(dir)
|
||||
switch name {
|
||||
case "detect_task_addition":
|
||||
msg, _ := op["message"].(string)
|
||||
isTask, conf, signals := q.detectTaskAddition(msg)
|
||||
return []any{isTask, conf, plainJSON(signals)}, ""
|
||||
case "add_message":
|
||||
msg, _ := op["message"].(string)
|
||||
channel, _ := op["channel"].(string)
|
||||
if channel == "" {
|
||||
channel = "chat"
|
||||
}
|
||||
var taskID *string
|
||||
if t, ok := op["task_id"].(string); ok && t != "" {
|
||||
taskID = &t
|
||||
}
|
||||
added := q.addMessage(msg, channel, taskID)
|
||||
return plainJSON(added.toDict()), ""
|
||||
case "get_pending":
|
||||
return plainJSONDicts(q.getPending()), ""
|
||||
case "approve":
|
||||
id, _ := op["msg_id"].(string)
|
||||
return q.approve(id), ""
|
||||
case "reject":
|
||||
id, _ := op["msg_id"].(string)
|
||||
return q.reject(id), ""
|
||||
case "approve_all":
|
||||
return q.approveAll(), ""
|
||||
case "clear":
|
||||
q.clear()
|
||||
return nil, ""
|
||||
case "get_approved":
|
||||
return plainJSONDicts(q.getApproved()), ""
|
||||
case "mark_executed":
|
||||
id, _ := op["msg_id"].(string)
|
||||
return q.markExecuted(id), ""
|
||||
case "get_status":
|
||||
return plainJSON(q.getStatus()), ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
// runEvalMain — the eval entry, called from main().
|
||||
func runEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "message-queue eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "message-queue eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := queueEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "message-queue eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
@@ -613,6 +613,9 @@ func truthRowCount() int {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runEvalMain(os.Args[2:]))
|
||||
}
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/paths.py). `paths eval <ops.json>` reads a JSON array of op
|
||||
// dicts and emits one result per op. Ops: repo_root, crate_dir,
|
||||
// deploy_root, rune_home, harness_scripts, wait_resume_dir.
|
||||
//
|
||||
// Fail-loud: unknown op aborts non-zero; the Python oracle stays behind
|
||||
// RUNE_PATHS_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func pathsEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := pathsEvalOp(name, op)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func pathsEvalOp(name string, op map[string]any) (any, string) {
|
||||
switch name {
|
||||
case "repo_root":
|
||||
return RepoRoot(), ""
|
||||
case "crate_dir":
|
||||
n, _ := op["name"].(string)
|
||||
return CrateDir(n), ""
|
||||
case "deploy_root":
|
||||
return DeployRoot(), ""
|
||||
case "rune_home":
|
||||
return RuneHome(), ""
|
||||
case "harness_scripts":
|
||||
return HarnessScripts(), ""
|
||||
case "wait_resume_dir":
|
||||
return WaitResumeDir(), ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func runPathsEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "paths eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "paths eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := pathsEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "paths eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
@@ -5,4 +5,10 @@
|
||||
// was ported — the Python paths.py is a library, not a CLI.
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
import "os"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runPathsEvalMain(os.Args[2:]))
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,8 @@ type anthropicChatResponse struct {
|
||||
Model string `json:"model"`
|
||||
Content []anthropicContentBlock `json:"content"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ func (c *AnthropicClient) ChatWithTools(ctx context.Context, model string, msgs
|
||||
return ChatReply{}, fmt.Errorf("%s /v1/messages decode: %w", c.provider, err)
|
||||
}
|
||||
reply := ChatReply{Model: out.Model, ContextLimit: contextLimitFor(c.ctxLimits, model),
|
||||
ContextUsed: out.Usage.InputTokens}
|
||||
ContextUsed: out.Usage.InputTokens, OutputTokens: out.Usage.OutputTokens}
|
||||
var content strings.Builder
|
||||
for _, blk := range out.Content {
|
||||
switch blk.Type {
|
||||
|
||||
@@ -97,7 +97,8 @@ type geminiChatResponse struct {
|
||||
} `json:"candidates"`
|
||||
ModelVersion string `json:"modelVersion"`
|
||||
UsageMetadata struct {
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||
} `json:"usageMetadata"`
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ func (c *GeminiClient) ChatWithTools(ctx context.Context, model string, msgs []C
|
||||
return ChatReply{}, fmt.Errorf("%s generateContent decode: %w", c.provider, err)
|
||||
}
|
||||
reply := ChatReply{Model: out.ModelVersion, ContextLimit: contextLimitFor(c.ctxLimits, model),
|
||||
ContextUsed: out.UsageMetadata.PromptTokenCount}
|
||||
ContextUsed: out.UsageMetadata.PromptTokenCount, OutputTokens: out.UsageMetadata.CandidatesTokenCount}
|
||||
var content strings.Builder
|
||||
if len(out.Candidates) > 0 {
|
||||
for _, part := range out.Candidates[0].Content.Parts {
|
||||
|
||||
@@ -89,7 +89,8 @@ type oiChatRequest struct {
|
||||
type oiChatResponse struct {
|
||||
Model string `json:"model"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
} `json:"usage"`
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
@@ -190,7 +191,7 @@ func (c *OpenAICompatClient) ChatWithTools(ctx context.Context, model string, ms
|
||||
return ChatReply{}, fmt.Errorf("%s /chat/completions decode: %w", c.provider, err)
|
||||
}
|
||||
reply := ChatReply{Model: out.Model, ContextLimit: contextLimitFor(c.ctxLimits, model),
|
||||
ContextUsed: out.Usage.PromptTokens}
|
||||
ContextUsed: out.Usage.PromptTokens, OutputTokens: out.Usage.CompletionTokens}
|
||||
if len(out.Choices) > 0 {
|
||||
msg := out.Choices[0].Message
|
||||
if msg.Content != nil {
|
||||
|
||||
@@ -86,7 +86,8 @@ func oiDecodeReply(resp *http.Response, model, provider string, ctxLimit int) (C
|
||||
type oiStreamChunk struct {
|
||||
Model string `json:"model"`
|
||||
Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
} `json:"usage"`
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
@@ -148,6 +149,9 @@ func (c *OpenAICompatClient) StreamChatWithTools(ctx context.Context, model stri
|
||||
if chunk.Usage.PromptTokens > 0 {
|
||||
reply.ContextUsed = chunk.Usage.PromptTokens
|
||||
}
|
||||
if chunk.Usage.CompletionTokens > 0 {
|
||||
reply.OutputTokens = chunk.Usage.CompletionTokens
|
||||
}
|
||||
if len(chunk.Choices) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -269,6 +273,9 @@ func (c *AnthropicClient) StreamChatWithTools(ctx context.Context, model string,
|
||||
PartialJSON string `json:"partial_json"`
|
||||
} `json:"delta"`
|
||||
Message *anthropicChatResponse `json:"message"`
|
||||
Usage *struct {
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &ev); err != nil {
|
||||
return fmt.Errorf("%s stream decode: %w", c.provider, err)
|
||||
@@ -279,6 +286,10 @@ func (c *AnthropicClient) StreamChatWithTools(ctx context.Context, model string,
|
||||
reply.Model = ev.Message.Model
|
||||
reply.ContextUsed = ev.Message.Usage.InputTokens
|
||||
}
|
||||
case "message_delta":
|
||||
if ev.Usage != nil && ev.Usage.OutputTokens > 0 {
|
||||
reply.OutputTokens = ev.Usage.OutputTokens
|
||||
}
|
||||
case "content_block_start":
|
||||
if ev.Block != nil && ev.Block.Type == "tool_use" {
|
||||
toolNames[ev.Index] = ev.Block.Name
|
||||
@@ -348,7 +359,7 @@ func anthropicDecodeReply(resp *http.Response, ctxLimit int, provider string) (C
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return ChatReply{}, fmt.Errorf("%s /v1/messages decode: %w", provider, err)
|
||||
}
|
||||
reply := ChatReply{Model: out.Model, ContextLimit: ctxLimit, ContextUsed: out.Usage.InputTokens}
|
||||
reply := ChatReply{Model: out.Model, ContextLimit: ctxLimit, ContextUsed: out.Usage.InputTokens, OutputTokens: out.Usage.OutputTokens}
|
||||
var content strings.Builder
|
||||
for _, blk := range out.Content {
|
||||
switch blk.Type {
|
||||
@@ -373,7 +384,7 @@ func geminiDecodeReply(resp *http.Response, ctxLimit int, provider string) (Chat
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
return ChatReply{}, fmt.Errorf("%s generateContent decode: %w", provider, err)
|
||||
}
|
||||
reply := ChatReply{Model: out.ModelVersion, ContextLimit: ctxLimit, ContextUsed: out.UsageMetadata.PromptTokenCount}
|
||||
reply := ChatReply{Model: out.ModelVersion, ContextLimit: ctxLimit, ContextUsed: out.UsageMetadata.PromptTokenCount, OutputTokens: out.UsageMetadata.CandidatesTokenCount}
|
||||
var content strings.Builder
|
||||
if len(out.Candidates) > 0 {
|
||||
for _, part := range out.Candidates[0].Content.Parts {
|
||||
@@ -455,6 +466,9 @@ func (c *GeminiClient) StreamChatWithTools(ctx context.Context, model string, ms
|
||||
if chunk.UsageMetadata.PromptTokenCount > 0 {
|
||||
reply.ContextUsed = chunk.UsageMetadata.PromptTokenCount
|
||||
}
|
||||
if chunk.UsageMetadata.CandidatesTokenCount > 0 {
|
||||
reply.OutputTokens = chunk.UsageMetadata.CandidatesTokenCount
|
||||
}
|
||||
if len(chunk.Candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrChannelBusy refuses a second in-flight turn on one channel — the
|
||||
@@ -79,6 +80,12 @@ type ChatReply struct {
|
||||
// guesses.
|
||||
ContextUsed int
|
||||
ContextLimit int
|
||||
// OutputTokens (D56 throughput telemetry): the tokens the provider
|
||||
// says it GENERATED this call (the ollama eval_count, the cloud
|
||||
// usage.completion_tokens family). Zero when the wire carries none
|
||||
// — the governor falls back to the assistant text's token estimate,
|
||||
// never to a guess presented as a measurement.
|
||||
OutputTokens int
|
||||
// ToolCalls (D45 toolset gap): the model's requested tool calls on
|
||||
// THIS response — empty on a final answer. Only ChatWithTools
|
||||
// returns these.
|
||||
@@ -152,6 +159,11 @@ type Engine struct {
|
||||
|
||||
flightsMu sync.Mutex
|
||||
inFlight map[string]bool // channel -> a turn is running
|
||||
|
||||
// governor (D56): the provider-aware dispatch governor — learns
|
||||
// tok/s from THIS engine's real turns (hot-state persisted), owns
|
||||
// lane pools and cloud-first routing. Never nil.
|
||||
governor *Governor
|
||||
}
|
||||
|
||||
// NewEngine builds the engine without a router: the local binding
|
||||
@@ -159,9 +171,13 @@ type Engine struct {
|
||||
// ErrProviderUnrouted. home is the rune home directory holding
|
||||
// provider_selection.json (the TUI's first-run law record).
|
||||
func NewEngine(store Store, client ModelClient, home string) *Engine {
|
||||
return &Engine{store: store, client: client, home: home, inFlight: map[string]bool{}}
|
||||
return &Engine{store: store, client: client, home: home, inFlight: map[string]bool{}, governor: NewGovernor(home)}
|
||||
}
|
||||
|
||||
// Governor exposes the engine's governor (the server surface reads it
|
||||
// for /v1/status; tests seed it directly).
|
||||
func (e *Engine) Governor() *Governor { return e.governor }
|
||||
|
||||
// NewRoutedEngine builds the engine with the cloud router attached:
|
||||
// the record's Provider field selects the client (the local spellings
|
||||
// keep the engine's own client; everything else resolves through the
|
||||
@@ -325,12 +341,30 @@ func (e *Engine) TurnStream(ctx context.Context, req TurnRequest, sink StreamSin
|
||||
payload = append(payload, cm)
|
||||
}
|
||||
|
||||
provider := "local"
|
||||
if recErr == nil {
|
||||
provider = rec.Provider
|
||||
}
|
||||
if servedByLocalBinding(provider) {
|
||||
provider = "local"
|
||||
}
|
||||
start := time.Now()
|
||||
reply, err := e.runTurn(ctx, client, model, req.Channel, payload, sink)
|
||||
if err != nil {
|
||||
// the user message stays persisted: the user DID send it (the
|
||||
// Python oracle's behavior); the failure is honest, not silent
|
||||
e.governor.RecordError(provider, err)
|
||||
return TurnResult{UserMsgID: userMsg.MsgID, Model: model}, err
|
||||
}
|
||||
// D56 throughput telemetry: the governor learns from the real
|
||||
// turn — the provider's own generated-token accounting when the
|
||||
// wire carries it, the assistant text's token estimate otherwise
|
||||
// (an honest estimate, never presented as a wire measurement).
|
||||
outTokens := reply.OutputTokens
|
||||
if outTokens <= 0 {
|
||||
outTokens = tokenEstimate(reply.Content)
|
||||
}
|
||||
e.governor.RecordTurn(provider, model, outTokens, time.Since(start), 1)
|
||||
|
||||
assistant := Message{
|
||||
MsgID: newMsgID(),
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
package runecore
|
||||
|
||||
// governor.go — the provider-aware dispatch governor (Gitea #2580 +
|
||||
// #2581, owner doctrine 2026-10-24: CLOUD-FIRST routing — cloud lanes
|
||||
// fill FIRST, speed is the priority; local absorbs only overflow; a
|
||||
// configured provider is spend-freely authorized, no consent gates).
|
||||
//
|
||||
// Everything here is learned from PRODUCTION samples (the engine's
|
||||
// real turn accounting): no synthetic benchmarks, no guessed curves.
|
||||
// Per-provider rolling samples persist under the hot-state pattern
|
||||
// (~/.rune/hot/provider_throughput.json) so the model survives
|
||||
// restarts.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HotThroughputFile is the hot-state path, relative to the rune home:
|
||||
// per-provider rolling throughput samples + the learned saturation
|
||||
// curve (the same survives-restart pattern as task_state.json).
|
||||
const HotThroughputFile = "hot/provider_throughput.json"
|
||||
|
||||
// sampleWindow is the rolling sample cap per provider (the model is
|
||||
// small and local: recent behavior only).
|
||||
const sampleWindow = 256
|
||||
|
||||
// DefaultCapFloor is the default marginal tok/s gain below which
|
||||
// another lane is judged useless (configurable: RUNE_GOVERNOR_CAP_FLOOR).
|
||||
const DefaultCapFloor = 5.0
|
||||
|
||||
// ThroughputSample is one production measurement: tokens the provider
|
||||
// actually generated in the wall time it took, at the lane count the
|
||||
// provider was serving.
|
||||
type ThroughputSample struct {
|
||||
At int64 `json:"at"` // unix seconds
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model,omitempty"`
|
||||
TokPerSec float64 `json:"tok_per_sec"`
|
||||
Lanes int `json:"lanes"`
|
||||
}
|
||||
|
||||
// ProviderHealth is the connection-health row (#2581): what the
|
||||
// governor actually knows about one provider's liveness.
|
||||
type ProviderHealth struct {
|
||||
Reachable bool `json:"reachable"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
ErrorRate float64 `json:"error_rate"` // recent errors / attempts
|
||||
TokPerSec float64 `json:"tok_per_sec"`
|
||||
LanesActive int `json:"lanes_active"`
|
||||
LanesCap int `json:"lanes_cap"`
|
||||
ThrottledUntil int64 `json:"throttled_until,omitempty"` // unix secs; a 429 ceiling
|
||||
LastSeen int64 `json:"last_seen,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// GovernorSnapshot is the honest external face: /v1/status's governor
|
||||
// section and the TUI's readout all render THIS — never more than the
|
||||
// governor measured.
|
||||
type GovernorSnapshot struct {
|
||||
TokensPerSec float64 `json:"tokens_per_sec"` // 0 = nothing measured yet
|
||||
LanesActive int `json:"lanes_active"`
|
||||
LanesCap int `json:"lanes_cap"`
|
||||
Providers map[string]ProviderHealth `json:"providers"`
|
||||
}
|
||||
|
||||
// governorState is the persisted hot-state shape.
|
||||
type governorState struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Samples map[string][]ThroughputSample `json:"samples"`
|
||||
// saturation is the learned curve: (provider, lanes) → EMA tok/s.
|
||||
Saturation map[string]map[int]float64 `json:"saturation"`
|
||||
}
|
||||
|
||||
// Governor is the dispatch governor. Zero-value is usable but inert;
|
||||
// NewGovernor loads the persisted hot state.
|
||||
type Governor struct {
|
||||
mu sync.Mutex
|
||||
home string
|
||||
state governorState
|
||||
health map[string]*ProviderHealth
|
||||
attempts map[string]int
|
||||
errors map[string]int
|
||||
lanes map[string]int // provider -> active lanes
|
||||
lastOrder []string // most-recently-used providers (the snapshot's headline tok/s)
|
||||
probed bool // at least one health probe ran in this process
|
||||
floor float64
|
||||
}
|
||||
|
||||
// GovernorCapFloorEnv resizes the marginal-gain floor (tok/s).
|
||||
const GovernorCapFloorEnv = "RUNE_GOVERNOR_CAP_FLOOR"
|
||||
|
||||
// NewGovernor builds a governor rooted at home (the rune home dir),
|
||||
// loading any persisted hot state. A missing or corrupt file is an
|
||||
// empty model, never an error (the hot-state freshness law).
|
||||
func NewGovernor(home string) *Governor {
|
||||
g := &Governor{
|
||||
home: home,
|
||||
state: governorState{
|
||||
SchemaVersion: 1,
|
||||
Samples: map[string][]ThroughputSample{},
|
||||
Saturation: map[string]map[int]float64{},
|
||||
},
|
||||
health: map[string]*ProviderHealth{},
|
||||
attempts: map[string]int{},
|
||||
errors: map[string]int{},
|
||||
lanes: map[string]int{},
|
||||
floor: DefaultCapFloor,
|
||||
}
|
||||
if v := strings.TrimSpace(os.Getenv(GovernorCapFloorEnv)); v != "" {
|
||||
if f, err := parsePositiveFloat(v); err == nil && f > 0 {
|
||||
g.floor = f
|
||||
}
|
||||
}
|
||||
g.load()
|
||||
return g
|
||||
}
|
||||
|
||||
func parsePositiveFloat(s string) (float64, error) {
|
||||
var f float64
|
||||
_, err := fmt.Sscanf(s, "%g", &f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
func (g *Governor) hotPath() string {
|
||||
return filepath.Join(g.home, HotThroughputFile)
|
||||
}
|
||||
|
||||
func (g *Governor) load() {
|
||||
data, err := os.ReadFile(g.hotPath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var st governorState
|
||||
if json.Unmarshal(data, &st) != nil || st.Samples == nil {
|
||||
return
|
||||
}
|
||||
if st.Saturation == nil {
|
||||
st.Saturation = map[string]map[int]float64{}
|
||||
}
|
||||
g.state = st
|
||||
}
|
||||
|
||||
// persist writes the hot state atomically (temp + rename — a reader
|
||||
// never sees a torn file). Best-effort: telemetry must never break a
|
||||
// turn.
|
||||
func (g *Governor) persist() {
|
||||
data, err := json.Marshal(g.state)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
path := g.hotPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// RecordTurn samples one completed turn: the provider that served it,
|
||||
// the tokens it generated (the client's own accounting; the engine's
|
||||
// honest token-estimate fallback when the wire carries none), the wall
|
||||
// duration, and the lane count it served at. This is the ONLY way the
|
||||
// model learns — production samples, never benchmarks.
|
||||
func (g *Governor) RecordTurn(provider, model string, outputTokens int, dur time.Duration, lanes int) {
|
||||
if provider == "" {
|
||||
provider = "local"
|
||||
}
|
||||
if lanes < 1 {
|
||||
lanes = 1
|
||||
}
|
||||
if outputTokens <= 0 || dur <= 0 {
|
||||
return // nothing measurable — the model never invents a rate
|
||||
}
|
||||
tps := float64(outputTokens) / dur.Seconds()
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now().Unix()
|
||||
s := ThroughputSample{At: now, Provider: provider, Model: model, TokPerSec: tps, Lanes: lanes}
|
||||
samples := append(g.state.Samples[provider], s)
|
||||
if len(samples) > sampleWindow {
|
||||
samples = samples[len(samples)-sampleWindow:]
|
||||
}
|
||||
g.state.Samples[provider] = samples
|
||||
// the saturation point: EMA over the lanes bucket (smooth, small)
|
||||
b := g.state.Saturation[provider]
|
||||
if b == nil {
|
||||
b = map[int]float64{}
|
||||
}
|
||||
b[lanes] = ema(b[lanes], tps)
|
||||
g.state.Saturation[provider] = b
|
||||
h := g.health[provider]
|
||||
if h == nil {
|
||||
h = &ProviderHealth{}
|
||||
g.health[provider] = h
|
||||
}
|
||||
h.Reachable, h.Authenticated = true, true
|
||||
h.TokPerSec = b[lanes]
|
||||
h.LastSeen = now
|
||||
h.LastError = ""
|
||||
g.touchOrder(provider)
|
||||
g.persist()
|
||||
}
|
||||
|
||||
// RecordError marks a failed attempt against a provider (the health
|
||||
// row's error rate).
|
||||
func (g *Governor) RecordError(provider string, err error) {
|
||||
if provider == "" {
|
||||
provider = "local"
|
||||
}
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.attempts[provider]++
|
||||
g.errors[provider]++
|
||||
h := g.health[provider]
|
||||
if h == nil {
|
||||
h = &ProviderHealth{}
|
||||
g.health[provider] = h
|
||||
}
|
||||
h.Reachable = false
|
||||
h.LastError = fmt.Sprint(err)
|
||||
if g.attempts[provider] > 0 {
|
||||
h.ErrorRate = float64(g.errors[provider]) / float64(g.attempts[provider])
|
||||
}
|
||||
}
|
||||
|
||||
// MarkThrottled records a 429: the published TPM/RPM ceiling just bit.
|
||||
// A throttled provider is WORSE than not spawning — routes refuse it
|
||||
// until the window lapses.
|
||||
func (g *Governor) MarkThrottled(provider string, until time.Time) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
h := g.health[provider]
|
||||
if h == nil {
|
||||
h = &ProviderHealth{}
|
||||
g.health[provider] = h
|
||||
}
|
||||
h.ThrottledUntil = until.Unix()
|
||||
}
|
||||
|
||||
// AcquireLane takes one lane on a provider (the pool's in-use count).
|
||||
func (g *Governor) AcquireLane(provider string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.lanes[provider]++
|
||||
}
|
||||
|
||||
// ReleaseLane returns one lane.
|
||||
func (g *Governor) ReleaseLane(provider string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if g.lanes[provider] > 0 {
|
||||
g.lanes[provider]--
|
||||
}
|
||||
}
|
||||
|
||||
// SaturationCap answers the lane cap for a provider: the highest lane
|
||||
// count whose MARGINAL tok/s gain still clears the floor. With no
|
||||
// curve learned, the honest cap is 1 (never invent headroom).
|
||||
func (g *Governor) SaturationCap(provider string) int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.saturationCapLocked(provider)
|
||||
}
|
||||
|
||||
func (g *Governor) saturationCapLocked(provider string) int {
|
||||
b := g.state.Saturation[provider]
|
||||
if len(b) == 0 {
|
||||
return 1
|
||||
}
|
||||
lanes := sortedLanes(b)
|
||||
cap := 1
|
||||
prev := b[lanes[0]]
|
||||
for _, l := range lanes[1:] {
|
||||
gain := b[l] - prev
|
||||
if gain < g.floor {
|
||||
break
|
||||
}
|
||||
cap = l
|
||||
prev = b[l]
|
||||
}
|
||||
return cap
|
||||
}
|
||||
|
||||
func sortedLanes(b map[int]float64) []int {
|
||||
out := make([]int, 0, len(b))
|
||||
for l := range b {
|
||||
out = append(out, l)
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Route applies the owner doctrine: CLOUD-FIRST — among healthy
|
||||
// candidates, every cloud provider (a non-local binding; configured
|
||||
// providers are spend-authorized, no consent gates) fills its lane
|
||||
// pool BEFORE any local provider absorbs overflow. Within a tier the
|
||||
// least-loaded provider wins (lanes/cap ratio). Unhealthy or
|
||||
// throttled providers are refused outright (route only to providers
|
||||
// actually alive). The second return is false when NO candidate is
|
||||
// healthy — the thin-pipe honesty case.
|
||||
func (g *Governor) Route(candidates []string) (ordered []string, healthy bool) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
var cloud, local []string
|
||||
now := time.Now().Unix()
|
||||
key := func(p string) float64 {
|
||||
h := g.health[p]
|
||||
c := 1
|
||||
if h != nil && h.LanesCap > 0 {
|
||||
c = h.LanesCap
|
||||
}
|
||||
return float64(g.lanes[p]) / float64(c)
|
||||
}
|
||||
less := func(a, b string) bool { return key(a) < key(b) }
|
||||
for _, p := range candidates {
|
||||
h := g.health[p]
|
||||
if h != nil {
|
||||
if !h.Reachable || (h.ThrottledUntil > 0 && h.ThrottledUntil > now) {
|
||||
continue // not alive / 429'd — worse than not spawning
|
||||
}
|
||||
} else if g.probed {
|
||||
// Health telemetry is live and this provider never came
|
||||
// back alive: refuse it (#2581 — route only to providers
|
||||
// actually alive). Before any probe ran, day-one routing
|
||||
// passes candidates through (never refuse what we never
|
||||
// asked).
|
||||
continue
|
||||
}
|
||||
if servedByLocalBinding(p) {
|
||||
local = append(local, p)
|
||||
} else {
|
||||
cloud = append(cloud, p)
|
||||
}
|
||||
}
|
||||
sort.Slice(cloud, func(i, j int) bool { return less(cloud[i], cloud[j]) })
|
||||
sort.Slice(local, func(i, j int) bool { return less(local[i], local[j]) })
|
||||
ordered = append(cloud, local...)
|
||||
return ordered, len(ordered) > 0
|
||||
}
|
||||
|
||||
// MarkReachable records a health probe outcome (#2581): reachable is
|
||||
// the probe's liveness verdict, authenticated whether the wire
|
||||
// accepted the key.
|
||||
func (g *Governor) MarkReachable(provider string, reachable, authenticated bool) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
h := g.health[provider]
|
||||
if h == nil {
|
||||
h = &ProviderHealth{}
|
||||
g.health[provider] = h
|
||||
}
|
||||
h.Reachable = reachable
|
||||
h.Authenticated = authenticated
|
||||
if reachable {
|
||||
h.LastSeen = time.Now().Unix()
|
||||
h.LastError = ""
|
||||
}
|
||||
g.probed = true
|
||||
}
|
||||
|
||||
func (g *Governor) touchOrder(p string) {
|
||||
for i, q := range g.lastOrder {
|
||||
if q == p {
|
||||
g.lastOrder = append(g.lastOrder[:i], g.lastOrder[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
g.lastOrder = append(g.lastOrder, p)
|
||||
}
|
||||
|
||||
// Snapshot renders the honest state: the headline tok/s is the most
|
||||
// recently sampled provider's current rate (0 when nothing was ever
|
||||
// measured — the bar shows '—', never a guess); lanes are the global
|
||||
// active count against the sum of the pools' caps.
|
||||
func (g *Governor) Snapshot() GovernorSnapshot {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
snap := GovernorSnapshot{Providers: map[string]ProviderHealth{}}
|
||||
for p, h := range g.health {
|
||||
row := *h
|
||||
row.LanesCap = g.saturationCapLocked(p)
|
||||
row.LanesActive = g.lanes[p]
|
||||
snap.Providers[p] = row
|
||||
}
|
||||
// headline tok/s: the freshest sample's provider, at the lane
|
||||
// count we actually run there — persisted state survives restarts
|
||||
// (lastOrder doesn't), so the read comes from the samples.
|
||||
var freshestP string
|
||||
var freshestAt int64
|
||||
for p, ss := range g.state.Samples {
|
||||
if len(ss) == 0 {
|
||||
continue
|
||||
}
|
||||
if last := ss[len(ss)-1]; last.At >= freshestAt {
|
||||
freshestAt, freshestP = last.At, p
|
||||
}
|
||||
}
|
||||
if freshestP != "" {
|
||||
lanes := g.lanes[freshestP]
|
||||
if lanes < 1 {
|
||||
lanes = 1
|
||||
}
|
||||
b := g.state.Saturation[freshestP]
|
||||
if v, ok := b[lanes]; ok {
|
||||
snap.TokensPerSec = v
|
||||
} else if len(b) > 0 {
|
||||
for _, l := range sortedLanes(b) {
|
||||
snap.TokensPerSec = b[l] // fall back to the freshest learned bucket
|
||||
}
|
||||
}
|
||||
}
|
||||
for p := range g.state.Saturation {
|
||||
if _, ok := snap.Providers[p]; !ok {
|
||||
cap := g.saturationCapLocked(p)
|
||||
snap.Providers[p] = ProviderHealth{Reachable: true, Authenticated: true, LanesCap: cap, LanesActive: g.lanes[p]}
|
||||
}
|
||||
snap.LanesActive += g.lanes[p]
|
||||
snap.LanesCap += g.saturationCapLocked(p)
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
// ema is the one-pole smooth: alpha small enough to ignore a single
|
||||
// outlier, big enough to track a real shift within a few turns.
|
||||
func ema(prev, next float64) float64 {
|
||||
if prev == 0 {
|
||||
return next
|
||||
}
|
||||
const alpha = 0.3
|
||||
return prev + alpha*(next-prev)
|
||||
}
|
||||
|
||||
// PollRegistryHealth probes every enabled, URL-bearing provider in
|
||||
// the landed registry (#2581): a cheap GET against the provider's
|
||||
// wire base with a short timeout. The verdict lands through
|
||||
// MarkReachable — reachable = the probe answered, authenticated = a
|
||||
// key ref is wired (the probe proves the WIRE, the ref proves the
|
||||
// credential is configured; a real 401 surfaces as an error sample).
|
||||
// Returns the number of providers probed.
|
||||
func (g *Governor) PollRegistryHealth(path string, timeout time.Duration) int {
|
||||
rows, err := LoadProviderRegistry(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
probed := 0
|
||||
for _, e := range rows {
|
||||
if !e.Enabled || strings.TrimSpace(e.BaseURL()) == "" {
|
||||
continue
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, perr := client.Get(strings.TrimRight(e.BaseURL(), "/") + "/models")
|
||||
if perr != nil {
|
||||
g.RecordError(e.Name, perr)
|
||||
g.MarkReachable(e.Name, false, e.APIKeyRef != "")
|
||||
probed++
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
g.MarkReachable(e.Name, true, e.APIKeyRef != "")
|
||||
probed++
|
||||
}
|
||||
return probed
|
||||
}
|
||||
|
||||
// HealthPollInterval is the live-state cadence.
|
||||
const HealthPollInterval = 60 * time.Second
|
||||
|
||||
// isCloudProvider answers the doctrine's tier split: anything NOT a
|
||||
// local binding is cloud (the same law the engine's router uses).
|
||||
func isCloudProvider(p string) bool { return !servedByLocalBinding(p) }
|
||||
@@ -0,0 +1,137 @@
|
||||
package runecore
|
||||
|
||||
// governor_test.go — the D56 governor's frozen laws: production-only
|
||||
// samples (a turn with no tokens is never a sample), the honest cap
|
||||
// (no curve = 1 lane, never invented headroom), cloud-first ordering
|
||||
// with health-gating and 429 refusal, and hot-state persistence.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGovernorRecordTurnSamplesAndPersists(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
g.RecordTurn("anthropic", "claude-x", 300, 10*time.Second, 1)
|
||||
snap := g.Snapshot()
|
||||
if snap.TokensPerSec != 30 {
|
||||
t.Fatalf("tok/s = %v, want 30", snap.TokensPerSec)
|
||||
}
|
||||
// hot state: a fresh governor over the same home sees the curve
|
||||
if _, err := os.Stat(filepath.Join(home, HotThroughputFile)); err != nil {
|
||||
t.Fatalf("hot state not persisted: %v", err)
|
||||
}
|
||||
g2 := NewGovernor(home)
|
||||
if got := g2.SaturationCap("anthropic"); got < 1 {
|
||||
t.Fatalf("cap after one sample = %d, want >=1", got)
|
||||
}
|
||||
if g2.Snapshot().TokensPerSec == 0 {
|
||||
t.Fatal("reloaded governor lost the measured tok/s")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorNoTokensNoSample(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
g.RecordTurn("anthropic", "m", 0, time.Second, 1)
|
||||
if snap := g.Snapshot(); snap.TokensPerSec != 0 || len(snap.Providers) != 0 {
|
||||
t.Fatalf("zero-token turn invented a sample: %+v", snap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorCapHonestWithoutCurve(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
if got := g.SaturationCap("never-measured"); got != 1 {
|
||||
t.Fatalf("cap with no curve = %d, want 1 (never invent headroom)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorCapFromMarginalGain(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
// 1 lane: 40 tok/s; 2 lanes: 70 (gain 30 >= floor); 3 lanes: 72
|
||||
// (gain 2 < floor) — the cap is 2 lanes.
|
||||
g.RecordTurn("p", "m", 400, 10*time.Second, 1)
|
||||
g.RecordTurn("p", "m", 700, 10*time.Second, 2)
|
||||
g.RecordTurn("p", "m", 720, 10*time.Second, 3)
|
||||
if got := g.SaturationCap("p"); got != 2 {
|
||||
t.Fatalf("cap = %d, want 2 (marginal gain at 3 lanes below the floor)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorRouteCloudFirstAndHealthGated(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
g.MarkReachable("ollama", true, true)
|
||||
g.MarkReachable("anthropic", true, true)
|
||||
g.MarkReachable("openai", true, true)
|
||||
g.MarkReachable("deadcloud", false, false)
|
||||
ordered, healthy := g.Route([]string{"ollama", "anthropic", "openai", "deadcloud"})
|
||||
if !healthy {
|
||||
t.Fatal("healthy candidates exist; Route lied about the thin pipe")
|
||||
}
|
||||
// cloud-first: both cloud rows precede local; deadcloud refused.
|
||||
foundCloud, foundLocal, sawDead := 0, 0, false
|
||||
for i, p := range ordered {
|
||||
switch p {
|
||||
case "anthropic", "openai":
|
||||
foundCloud++
|
||||
if foundLocal > 0 {
|
||||
t.Fatalf("cloud row %s routed after local — doctrine broken (order %v)", p, ordered)
|
||||
}
|
||||
case "ollama":
|
||||
foundLocal++
|
||||
case "deadcloud":
|
||||
sawDead = true
|
||||
}
|
||||
_ = i
|
||||
}
|
||||
if foundCloud != 2 || foundLocal != 1 || sawDead {
|
||||
t.Fatalf("route order wrong: %v", ordered)
|
||||
}
|
||||
// 429 throttle = worse than not spawning
|
||||
g.MarkThrottled("openai", time.Now().Add(time.Hour))
|
||||
ordered, _ = g.Route([]string{"openai"})
|
||||
if len(ordered) != 0 {
|
||||
t.Fatalf("throttled provider routed: %v", ordered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorThinPipeHonesty(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
// health telemetry live: the probe ran, the provider never came
|
||||
// back alive — the route must refuse and report the thin pipe.
|
||||
g.MarkReachable("ollama", false, false)
|
||||
_, healthy := g.Route([]string{"ollama"})
|
||||
if healthy {
|
||||
t.Fatal("dead provider routed: Route must report the thin pipe")
|
||||
}
|
||||
// day-one honesty in the other direction: before ANY probe ran,
|
||||
// routing passes through (never refuse what we never asked).
|
||||
fresh := NewGovernor(t.TempDir())
|
||||
_, healthy = fresh.Route([]string{"ollama"})
|
||||
if !healthy {
|
||||
t.Fatal("pre-probe routing must pass through, not refuse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernorLanesCount(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
g := NewGovernor(home)
|
||||
g.RecordTurn("anthropic", "m", 100, time.Second, 1)
|
||||
g.AcquireLane("anthropic")
|
||||
g.AcquireLane("anthropic")
|
||||
snap := g.Snapshot()
|
||||
if snap.Providers["anthropic"].LanesActive != 2 || snap.LanesActive != 2 {
|
||||
t.Fatalf("lanes = %+v, want 2", snap)
|
||||
}
|
||||
g.ReleaseLane("anthropic")
|
||||
if snap := g.Snapshot(); snap.Providers["anthropic"].LanesActive != 1 {
|
||||
t.Fatalf("lanes after release = %d, want 1", snap.Providers["anthropic"].LanesActive)
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (c *OllamaClient) ChatWithTools(ctx context.Context, model string, msgs []C
|
||||
// server-side for qwen3-family even without think:true).
|
||||
visible, inline := splitInlineThinking(out.Message.Content)
|
||||
reply := ChatReply{Content: visible, Model: out.Model,
|
||||
ContextUsed: out.PromptEvalCount}
|
||||
ContextUsed: out.PromptEvalCount, OutputTokens: out.EvalCount}
|
||||
for _, tc := range out.Message.ToolCalls {
|
||||
call := ToolCall{Name: tc.Function.Name}
|
||||
if len(tc.Function.Arguments) > 0 {
|
||||
@@ -323,7 +323,7 @@ func (c *OllamaClient) StreamChatWithTools(ctx context.Context, model string, ms
|
||||
return ChatReply{}, w.err(fmt.Errorf("ollama stream read: %w", err), idle)
|
||||
}
|
||||
visible, inline := splitInlineThinking(content.String())
|
||||
reply := ChatReply{Content: visible, Model: last.Model, ContextUsed: last.PromptEvalCount}
|
||||
reply := ChatReply{Content: visible, Model: last.Model, ContextUsed: last.PromptEvalCount, OutputTokens: last.EvalCount}
|
||||
reply.ToolCalls = toolCalls
|
||||
reply.ContextLimit = c.contextLimit(ctx, model)
|
||||
if thinking.Len() > 0 {
|
||||
|
||||
@@ -38,6 +38,16 @@ type RegistryModel struct {
|
||||
ContextLimit int // the row's limit.context; 0 = the row records none
|
||||
}
|
||||
|
||||
// BaseURL is the provider's wire base: the migrated /v1-bearing
|
||||
// SourceBaseURL when present, the raw URL otherwise ("" when the row
|
||||
// carries none).
|
||||
func (e ProviderRegistryEntry) BaseURL() string {
|
||||
if e.SourceBaseURL != "" {
|
||||
return e.SourceBaseURL
|
||||
}
|
||||
return e.URL
|
||||
}
|
||||
|
||||
// DefaultProviderRegistryPath resolves the registry file by the
|
||||
// migration's own law (the TUI's registry.go mirror): RUNE_REGISTRY_
|
||||
// CONFIG overrides, the user config dir's rune/providers.json is the
|
||||
|
||||
@@ -120,9 +120,18 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// (used = tokens sent, limit = the model's budget). Absent
|
||||
// until the first turn — the bar never guesses.
|
||||
"context": map[string]int{"used": ctxUsed, "limit": ctxLimit},
|
||||
// D56 dispatch governor: the honest live state — measured tok/s
|
||||
// (0 = nothing measured yet; the bar renders '—', never a
|
||||
// guess), lanes in use against the learned cap, and the
|
||||
// per-provider health rows (#2581).
|
||||
"governor": s.engine.Governor().Snapshot(),
|
||||
})
|
||||
}
|
||||
|
||||
// Engine exposes the served engine (main.go wires the governor's
|
||||
// health poll through it).
|
||||
func (s *Server) Engine() *Engine { return s.engine }
|
||||
|
||||
// handleHealth: the core's own liveness + identity.
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
var model string
|
||||
|
||||
@@ -115,6 +115,18 @@ func main() {
|
||||
router := runecore.NewProviderRouter(runecore.SecretToolReader())
|
||||
srv := runecore.NewServer(runecore.NewRoutedEngine(st, client, home, router), client, home)
|
||||
|
||||
// D56 connection health (#2581): poll the registry's providers'
|
||||
// live state so the governor routes only to providers actually
|
||||
// alive — once at boot, then on the health cadence.
|
||||
gov := srv.Engine().Governor()
|
||||
go func() {
|
||||
registry := runecore.DefaultProviderRegistryPath()
|
||||
for {
|
||||
gov.PollRegistryHealth(registry, 5*time.Second)
|
||||
time.Sleep(runecore.HealthPollInterval)
|
||||
}
|
||||
}()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
httpSrv := &http.Server{Addr: addr, Handler: srv.Handler()}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Eval — the live ops surface for the Python delegation facade
|
||||
// (src/rune/voicebox.py). `voicebox eval <ops.json>` reads a JSON array
|
||||
// of op dicts and emits one result per op. Ops: handle_hook, manifest.
|
||||
//
|
||||
// The empty-event validation improvement applies here too (documented:
|
||||
// the oracle returns ok for a silent no-op; the engine rejects it).
|
||||
//
|
||||
// Fail-loud: unknown op or engine error aborts non-zero; the Python
|
||||
// oracle stays behind RUNE_VOICEBOX_ENGINE=python.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func voiceboxEvalOps(ops []map[string]any) ([]any, string) {
|
||||
out := make([]any, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
name, _ := op["op"].(string)
|
||||
res, emsg := voiceboxEvalOp(name, op)
|
||||
if emsg != "" {
|
||||
return nil, fmt.Sprintf("op %d (%s): %s", i, name, emsg)
|
||||
}
|
||||
out = append(out, res)
|
||||
}
|
||||
return out, ""
|
||||
}
|
||||
|
||||
func voiceboxEvalOp(name string, op map[string]any) (any, string) {
|
||||
switch name {
|
||||
case "handle_hook":
|
||||
event, _ := op["event"].(string)
|
||||
data, _ := op["data"].(map[string]any)
|
||||
return handleHook(event, data), ""
|
||||
case "manifest":
|
||||
return plugin, ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unknown op %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func runVoiceboxEvalMain(args []string) int {
|
||||
var raw []byte
|
||||
var err error
|
||||
if len(args) > 0 && args[0] != "-" {
|
||||
raw, err = os.ReadFile(args[0])
|
||||
} else {
|
||||
raw, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "voicebox eval: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
var ops []map[string]any
|
||||
if jerr := json.Unmarshal(raw, &ops); jerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "voicebox eval: parse ops: %v\n", jerr)
|
||||
return 2
|
||||
}
|
||||
results, emsg := voiceboxEvalOps(ops)
|
||||
if emsg != "" {
|
||||
fmt.Fprintf(os.Stderr, "voicebox eval: %s\n", emsg)
|
||||
return 1
|
||||
}
|
||||
enc, _ := json.Marshal(results)
|
||||
fmt.Println(string(enc))
|
||||
return 0
|
||||
}
|
||||
@@ -43,6 +43,9 @@ var plugin = map[string]any{
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "eval" {
|
||||
os.Exit(runVoiceboxEvalMain(os.Args[2:]))
|
||||
}
|
||||
if len(os.Args) < 2 || os.Args[1] != "replay" {
|
||||
fmt.Fprintln(os.Stderr, "usage: voicebox replay <truth.json>")
|
||||
os.Exit(2)
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.351.0"
|
||||
__version__ = "0.358.1"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
+123
-11
@@ -26,7 +26,69 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): the read/report
|
||||
# seams resolve through the Go engine (src/go/abstraction, `eval
|
||||
# <ops.json>` surface) against the SAME abstraction.db the oracle body
|
||||
# reads/writes. Python bodies kept as oracle/escape hatch
|
||||
# (RUNE_ABSTRACTION_ENGINE=python); RUNE_ABSTRACTION_BIN overrides the
|
||||
# binary. Missing binary or engine error raises RuntimeError — no silent
|
||||
# fallback (house shape: model_routing.reconcile_tools_support).
|
||||
# extract_facts/synthesize_observations/run_lifecycle stay Python this
|
||||
# wave: they embed the LLM-as-a-Judge and lifecycle-transition seams the
|
||||
# eval surface does not govern (recorded in the integrated receipt).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_ABSTRACTION_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_ABSTRACTION_BIN")
|
||||
if override:
|
||||
return override
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "src", "go", "abstraction", "abstraction-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"abstraction go engine binary missing: {bin_path} (set RUNE_ABSTRACTION_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="abs-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"abstraction go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"abstraction go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _memory_dir() -> str:
|
||||
return memory_env.memory_dir()
|
||||
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -51,7 +113,6 @@ _ABSTRACTION_STOPWORDS_FROZEN = frozenset(
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _ctx_budget(model_id: str) -> int:
|
||||
"""#28: model-aware num_ctx (never the hardcoded 8192)."""
|
||||
try:
|
||||
@@ -60,6 +121,7 @@ def _ctx_budget(model_id: str) -> int:
|
||||
import model_registry as _mr
|
||||
return _mr.resolve_context_budget(model_id)["num_ctx"]
|
||||
|
||||
|
||||
def _ABSTRACTION_STOPWORDS() -> list:
|
||||
"""Gather seam (W3.4): the fact-extraction stopword table as a sorted
|
||||
list — data received by the kernel; identical filter on both engines."""
|
||||
@@ -74,6 +136,7 @@ def _core():
|
||||
|
||||
def _cc_probe():
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
try:
|
||||
return _cl if _cl.available() else None
|
||||
except Exception: # noqa: BLE001, S110 — loader probe never crashes the fallback
|
||||
@@ -81,8 +144,7 @@ def _cc_probe():
|
||||
|
||||
|
||||
def cc_loader_pin() -> bool:
|
||||
"""The env pin: RUNE_COGNITION_CORE=python → kernels OFF (shared probe).
|
||||
"""
|
||||
"""The env pin: RUNE_COGNITION_CORE=python → kernels OFF (shared probe)."""
|
||||
import os
|
||||
|
||||
return os.environ.get("RUNE_COGNITION_CORE", "auto") == "python"
|
||||
@@ -306,18 +368,24 @@ Extract 1-5 key facts. Be precise, not verbose."""
|
||||
try:
|
||||
from rune import cc_loader as _cl
|
||||
|
||||
raw = _cl.abstraction_extract_facts(
|
||||
episode_text, _ABSTRACTION_STOPWORDS(), 10)
|
||||
raw = _cl.abstraction_extract_facts(episode_text, _ABSTRACTION_STOPWORDS(), 10)
|
||||
for item in raw or []:
|
||||
text_frag = str(item.get("text", "")).strip()
|
||||
if not text_frag:
|
||||
continue
|
||||
facts.append({
|
||||
"what": text_frag, "when": "", "where": "", "who": "",
|
||||
"why": "", "type": "experience", "entities": [],
|
||||
"kernel": True,
|
||||
"content_words": item.get("content_words"),
|
||||
})
|
||||
facts.append(
|
||||
{
|
||||
"what": text_frag,
|
||||
"when": "",
|
||||
"where": "",
|
||||
"who": "",
|
||||
"why": "",
|
||||
"type": "experience",
|
||||
"entities": [],
|
||||
"kernel": True,
|
||||
"content_words": item.get("content_words"),
|
||||
}
|
||||
)
|
||||
except Exception: # noqa: BLE001, S110 — kernel failure degrades to empty, intentional silence
|
||||
pass
|
||||
|
||||
@@ -475,6 +543,13 @@ Respond as JSON array:
|
||||
|
||||
def list_beliefs(limit=20):
|
||||
"""List active beliefs."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "list_beliefs", "memory_dir": _memory_dir(), "limit": limit}])[0]
|
||||
return _list_beliefs_python(limit)
|
||||
|
||||
|
||||
def _list_beliefs_python(limit=20):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
@@ -487,6 +562,13 @@ def list_beliefs(limit=20):
|
||||
|
||||
def list_rules(limit=20):
|
||||
"""List active rules."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "list_rules", "memory_dir": _memory_dir(), "limit": limit}])[0]
|
||||
return _list_rules_python(limit)
|
||||
|
||||
|
||||
def _list_rules_python(limit=20):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
@@ -499,6 +581,13 @@ def list_rules(limit=20):
|
||||
|
||||
def list_skills(limit=20):
|
||||
"""List active skills."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "list_skills", "memory_dir": _memory_dir(), "limit": limit}])[0]
|
||||
return _list_skills_python(limit)
|
||||
|
||||
|
||||
def _list_skills_python(limit=20):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
try:
|
||||
rows = db.execute(
|
||||
@@ -543,6 +632,13 @@ def run_lifecycle():
|
||||
|
||||
def pipeline_stats():
|
||||
"""Return pipeline statistics."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "pipeline_stats", "memory_dir": _memory_dir()}])[0]
|
||||
return _pipeline_stats_python()
|
||||
|
||||
|
||||
def _pipeline_stats_python():
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
stats = {}
|
||||
tables_with_state = {"facts", "observations", "beliefs", "rules", "skills"}
|
||||
@@ -559,6 +655,22 @@ def pipeline_stats():
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""Hook handler — extract facts from session end events."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "handle_hook",
|
||||
"memory_dir": _memory_dir(),
|
||||
"event": event,
|
||||
"data": data,
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _handle_hook_python(event, data)
|
||||
|
||||
|
||||
def _handle_hook_python(event, data=None):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
if event == "on_session_end" and data:
|
||||
# Could extract facts from session summary
|
||||
return {"status": "ok", "event": event, "handler": "abstraction"}
|
||||
|
||||
@@ -8,6 +8,61 @@ are activated automatically, without the model needing to remember to call them.
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): build_auto_context
|
||||
# and load_active_tasks resolve through the Go engine (src/go/auto_context,
|
||||
# `eval <ops.json>` surface). Python bodies kept as oracle/escape hatch
|
||||
# (RUNE_AUTO_CONTEXT_ENGINE=python); RUNE_AUTO_CONTEXT_BIN overrides the
|
||||
# binary. Missing binary or engine error raises RuntimeError — no silent
|
||||
# fallback (house shape: model_routing.reconcile_tools_support).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_AUTO_CONTEXT_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_AUTO_CONTEXT_BIN")
|
||||
if override:
|
||||
return override
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "src", "go", "auto_context", "auto-context-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"auto-context go engine binary missing: {bin_path} "
|
||||
"(set RUNE_AUTO_CONTEXT_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="ac-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"auto-context go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"auto-context go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
@@ -68,6 +123,20 @@ def run_always_digest():
|
||||
|
||||
def load_active_tasks():
|
||||
"""Load active tasks from the persistence ledger."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "load_active_tasks",
|
||||
"memory_dir": memory_env.memory_dir(),
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _load_active_tasks_python()
|
||||
|
||||
|
||||
def _load_active_tasks_python():
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
tasks_file = os.path.join(memory_env.memory_dir(), "tasks", "persisted.json")
|
||||
if not os.path.exists(tasks_file):
|
||||
return []
|
||||
@@ -84,6 +153,21 @@ def load_active_tasks():
|
||||
|
||||
def build_auto_context(message):
|
||||
"""Build automatic context from warm neurons, memory recall, and tasks."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "build_auto_context",
|
||||
"memory_dir": memory_env.memory_dir(),
|
||||
"message": message,
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _build_auto_context_python(message)
|
||||
|
||||
|
||||
def _build_auto_context_python(message):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
context = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"message_preview": message[:200] if message else "",
|
||||
|
||||
@@ -26,6 +26,66 @@ import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): the cache seam
|
||||
# operations resolve through the Go engine (src/go/cache, `eval
|
||||
# <ops.json>` surface) against the SAME cache.db the oracle body
|
||||
# reads/writes. Python bodies kept as oracle/escape hatch
|
||||
# (RUNE_CACHE_ENGINE=python); RUNE_CACHE_BIN overrides the binary.
|
||||
# Missing binary or engine error raises RuntimeError — no silent
|
||||
# fallback (house shape: model_routing.reconcile_tools_support).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_CACHE_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_CACHE_BIN")
|
||||
if override:
|
||||
return override
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "src", "go", "cache", "cache-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"cache go engine binary missing: {bin_path} (set RUNE_CACHE_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="cache-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"cache go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"cache go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _memory_dir() -> str:
|
||||
return memory_env.memory_dir()
|
||||
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -183,6 +243,16 @@ def get_cache(key):
|
||||
Hit/miss statistics are accumulated in memory and flushed to SQLite
|
||||
periodically (every 50 events or 30 seconds) to avoid write-on-read.
|
||||
"""
|
||||
if _go_engine_enabled():
|
||||
row = _go_eval([{"op": "get_cache", "memory_dir": _memory_dir(), "key": key}])[0]
|
||||
if row is None:
|
||||
return {"hit": False, "key": key}
|
||||
return row
|
||||
return _get_cache_python(key)
|
||||
|
||||
|
||||
def _get_cache_python(key):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
try:
|
||||
row = db.execute("SELECT * FROM cache WHERE key=? AND invalidated_at IS NULL", (key,)).fetchone()
|
||||
@@ -225,6 +295,26 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
|
||||
|
||||
Uses INSERT … ON CONFLICT to update content without resetting hit/miss stats.
|
||||
"""
|
||||
if _go_engine_enabled():
|
||||
flush_stats()
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "put_cache",
|
||||
"memory_dir": _memory_dir(),
|
||||
"key": key,
|
||||
"value": value,
|
||||
"compressed": compressed or "",
|
||||
"scope": scope,
|
||||
"source_hash": source_hash,
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _put_cache_python(key, value, compressed=compressed, scope=scope, source_hash=source_hash)
|
||||
|
||||
|
||||
def _put_cache_python(key, value, compressed=None, scope="general", source_hash=""):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
# Flush any pending stats before writing so we don't lose counts
|
||||
flush_stats()
|
||||
|
||||
@@ -259,6 +349,13 @@ def put_cache(key, value, compressed=None, scope="general", source_hash=""):
|
||||
|
||||
def invalidate(key, reason=""):
|
||||
"""Invalidate a specific cache entry."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "invalidate", "memory_dir": _memory_dir(), "key": key, "reason": reason}])[0]
|
||||
return _invalidate_python({"op": "invalidate", "memory_dir": _memory_dir(), "key": key, "reason": reason})
|
||||
|
||||
|
||||
def _invalidate_python(key, reason=""):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
db.execute("UPDATE cache SET invalidated_at=? WHERE key=? AND invalidated_at IS NULL", (_now(), key))
|
||||
db.commit()
|
||||
@@ -268,6 +365,15 @@ def invalidate(key, reason=""):
|
||||
|
||||
def invalidate_scope(scope, reason=""):
|
||||
"""Invalidate all entries in a scope."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "invalidate_scope", "memory_dir": _memory_dir(), "scope": scope, "reason": reason}])[0]
|
||||
return _invalidate_scope_python(
|
||||
{"op": "invalidate_scope", "memory_dir": _memory_dir(), "scope": scope, "reason": reason}
|
||||
)
|
||||
|
||||
|
||||
def _invalidate_scope_python(scope, reason=""):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
count = db.execute(
|
||||
"UPDATE cache SET invalidated_at=? WHERE scope=? AND invalidated_at IS NULL", (_now(), scope)
|
||||
@@ -279,6 +385,13 @@ def invalidate_scope(scope, reason=""):
|
||||
|
||||
def invalidate_all(reason=""):
|
||||
"""Invalidate everything. Nuclear option."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "invalidate_all", "memory_dir": _memory_dir(), "reason": reason}])[0]
|
||||
return _invalidate_all_python({"op": "invalidate_all", "memory_dir": _memory_dir(), "reason": reason})
|
||||
|
||||
|
||||
def _invalidate_all_python(reason=""):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
db = connect()
|
||||
count = db.execute("UPDATE cache SET invalidated_at=? WHERE invalidated_at IS NULL", (_now(),)).rowcount
|
||||
db.commit()
|
||||
@@ -350,6 +463,14 @@ def _parse_ts(ts_str):
|
||||
|
||||
def cache_stats():
|
||||
"""Return cache statistics (includes in-memory pending counts)."""
|
||||
if _go_engine_enabled():
|
||||
flush_stats()
|
||||
return _go_eval([{"op": "cache_stats", "memory_dir": _memory_dir()}])[0]
|
||||
return _cache_stats_python()
|
||||
|
||||
|
||||
def _cache_stats_python():
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
# Flush pending counts first so stats are fully accurate
|
||||
flush_stats()
|
||||
|
||||
@@ -380,6 +501,22 @@ def cache_stats():
|
||||
|
||||
def handle_hook(event, data=None):
|
||||
"""Event-driven invalidation. Subscribe to event bus events."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "handle_hook",
|
||||
"memory_dir": _memory_dir(),
|
||||
"event": event,
|
||||
"data": data or {},
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _handle_hook_python(event, data)
|
||||
|
||||
|
||||
def _handle_hook_python(event, data=None):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
if event in INVALIDATION_MAP:
|
||||
scopes = INVALIDATION_MAP[event]
|
||||
invalidated = 0
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.351.0"' in rune_version,
|
||||
'__version__ = "0.358.1"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -18,11 +18,66 @@ Usage:
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): the queue seam
|
||||
# operations resolve through the Go engine (src/go/message_queue, `eval
|
||||
# <ops.json>` surface) against the SAME queue.json the oracle body
|
||||
# reads/writes. Python bodies kept as oracle/escape hatch
|
||||
# (RUNE_MESSAGE_QUEUE_ENGINE=python); RUNE_MESSAGE_QUEUE_BIN overrides the
|
||||
# binary. Missing binary or engine error raises RuntimeError — no silent
|
||||
# fallback (house shape: model_routing.reconcile_tools_support).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_MESSAGE_QUEUE_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_MESSAGE_QUEUE_BIN")
|
||||
if override:
|
||||
return override
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "src", "go", "message_queue", "message-queue-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"message-queue go engine binary missing: {bin_path} "
|
||||
"(set RUNE_MESSAGE_QUEUE_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="mq-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"message-queue go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"message-queue go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -90,6 +145,13 @@ class MessageQueue:
|
||||
def __init__(self):
|
||||
os.makedirs(QUEUE_DIR, exist_ok=True)
|
||||
self.queue: List[QueuedMessage] = self._load_queue()
|
||||
if _go_engine_enabled():
|
||||
self._sync_from_engine()
|
||||
|
||||
def _sync_from_engine(self):
|
||||
"""Reload the in-memory mirror from the queue file the Go engine
|
||||
mutates (see module header)."""
|
||||
self.queue = self._load_queue()
|
||||
|
||||
def _load_queue(self) -> List[QueuedMessage]:
|
||||
"""Load queue from file."""
|
||||
@@ -109,6 +171,21 @@ class MessageQueue:
|
||||
|
||||
Returns: (is_task_addition, confidence, signals)
|
||||
"""
|
||||
if _go_engine_enabled():
|
||||
row = _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "detect_task_addition",
|
||||
"queue_dir": QUEUE_DIR,
|
||||
"message": message,
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return bool(row[0]), float(row[1]), list(row[2])
|
||||
return self._detect_task_addition_python(message)
|
||||
|
||||
def _detect_task_addition_python(self, message: str) -> tuple[bool, float, List[str]]:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
signals = []
|
||||
confidence = 0.0
|
||||
message_lower = message.lower()
|
||||
@@ -160,6 +237,24 @@ class MessageQueue:
|
||||
|
||||
def add_message(self, message: str, channel: str, task_id: Optional[str] = None) -> QueuedMessage:
|
||||
"""Add a message to the queue."""
|
||||
if _go_engine_enabled():
|
||||
row = _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "add_message",
|
||||
"queue_dir": QUEUE_DIR,
|
||||
"message": message,
|
||||
"channel": channel,
|
||||
"task_id": task_id,
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
self._sync_from_engine()
|
||||
return QueuedMessage(**row)
|
||||
return self._add_message_python(message, channel, task_id)
|
||||
|
||||
def _add_message_python(self, message: str, channel: str, task_id: Optional[str] = None) -> QueuedMessage:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
is_task, confidence, signals = self.detect_task_addition(message)
|
||||
|
||||
msg_id = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
|
||||
@@ -184,6 +279,14 @@ class MessageQueue:
|
||||
|
||||
def approve(self, msg_id: str) -> bool:
|
||||
"""Approve a queued message."""
|
||||
if _go_engine_enabled():
|
||||
got = _go_eval([{"op": "approve", "queue_dir": QUEUE_DIR, "msg_id": msg_id}])[0]
|
||||
self._sync_from_engine()
|
||||
return bool(got)
|
||||
return self._approve_python(msg_id)
|
||||
|
||||
def _approve_python(self, msg_id: str) -> bool:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
for msg in self.queue:
|
||||
if msg.id == msg_id:
|
||||
msg.status = "approved"
|
||||
@@ -193,6 +296,14 @@ class MessageQueue:
|
||||
|
||||
def reject(self, msg_id: str) -> bool:
|
||||
"""Reject a queued message."""
|
||||
if _go_engine_enabled():
|
||||
got = _go_eval([{"op": "reject", "queue_dir": QUEUE_DIR, "msg_id": msg_id}])[0]
|
||||
self._sync_from_engine()
|
||||
return bool(got)
|
||||
return self._reject_python(msg_id)
|
||||
|
||||
def _reject_python(self, msg_id: str) -> bool:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
for msg in self.queue:
|
||||
if msg.id == msg_id:
|
||||
msg.status = "rejected"
|
||||
@@ -202,6 +313,14 @@ class MessageQueue:
|
||||
|
||||
def approve_all(self) -> int:
|
||||
"""Approve all pending messages."""
|
||||
if _go_engine_enabled():
|
||||
got = _go_eval([{"op": "approve_all", "queue_dir": QUEUE_DIR}])[0]
|
||||
self._sync_from_engine()
|
||||
return int(got)
|
||||
return self._approve_all_python()
|
||||
|
||||
def _approve_all_python(self) -> int:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
count = 0
|
||||
for msg in self.queue:
|
||||
if msg.status == "pending":
|
||||
@@ -212,6 +331,14 @@ class MessageQueue:
|
||||
|
||||
def clear(self):
|
||||
"""Clear all messages."""
|
||||
if _go_engine_enabled():
|
||||
_go_eval([{"op": "clear", "queue_dir": QUEUE_DIR}])
|
||||
self._sync_from_engine()
|
||||
return
|
||||
return self._clear_python()
|
||||
|
||||
def _clear_python(self):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
self.queue = []
|
||||
self._save_queue()
|
||||
|
||||
@@ -221,6 +348,14 @@ class MessageQueue:
|
||||
|
||||
def mark_executed(self, msg_id: str) -> bool:
|
||||
"""Mark a message as executed."""
|
||||
if _go_engine_enabled():
|
||||
got = _go_eval([{"op": "mark_executed", "queue_dir": QUEUE_DIR, "msg_id": msg_id}])[0]
|
||||
self._sync_from_engine()
|
||||
return bool(got)
|
||||
return self._mark_executed_python(msg_id)
|
||||
|
||||
def _mark_executed_python(self, msg_id: str) -> bool:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
for msg in self.queue:
|
||||
if msg.id == msg_id:
|
||||
msg.status = "executed"
|
||||
@@ -230,6 +365,12 @@ class MessageQueue:
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get queue status."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "get_status", "queue_dir": QUEUE_DIR}])[0]
|
||||
return self._get_status_python()
|
||||
|
||||
def _get_status_python(self) -> Dict[str, Any]:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
pending = len([m for m in self.queue if m.status == "pending"])
|
||||
approved = len([m for m in self.queue if m.status == "approved"])
|
||||
executed = len([m for m in self.queue if m.status == "executed"])
|
||||
|
||||
+102
-6
@@ -13,41 +13,137 @@ deployment needs them:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): every resolver
|
||||
# resolves through the Go engine (src/go/paths, `eval <ops.json>`
|
||||
# surface). Python bodies kept as oracle/escape hatch
|
||||
# (RUNE_PATHS_ENGINE=python); RUNE_PATHS_BIN overrides the binary.
|
||||
# Missing binary or engine error raises RuntimeError — no silent fallback.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_PATHS_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_PATHS_BIN")
|
||||
if override:
|
||||
return override
|
||||
return os.path.join(REPO_ROOT, "src", "go", "paths", "paths-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"go-paths go engine binary missing: {bin_path} (set RUNE_PATHS_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="paths-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"go-paths go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"go-paths go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# This file: src/rune/paths.py → repo root is THREE levels up
|
||||
# (src/rune/paths.py → src/rune → src → repo).
|
||||
REPO_ROOT = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def repo_root() -> str:
|
||||
"""Fail-loud Go delegation (see module header)."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "repo_root"}])[0]
|
||||
return _repo_root_python()
|
||||
|
||||
|
||||
def _repo_root_python() -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
"""The rune repository root (derived from this file's location)."""
|
||||
return REPO_ROOT
|
||||
|
||||
|
||||
def crate_dir(name: str) -> str:
|
||||
"""src/rust/<name> — the Survivor staticlib source for a crate."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "crate_dir", "name": name}])[0]
|
||||
return _crate_dir_python(name)
|
||||
|
||||
|
||||
def _crate_dir_python(name: str) -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
return os.path.join(REPO_ROOT, "src", "rust", name)
|
||||
|
||||
|
||||
def deploy_root() -> str:
|
||||
"""Fail-loud Go delegation (see module header)."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "deploy_root"}])[0]
|
||||
return _deploy_root_python()
|
||||
|
||||
|
||||
def _deploy_root_python() -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
"""The deployed tree root (env override, workdrive default)."""
|
||||
return os.environ.get("RUNE_DEPLOY_ROOT", "/mnt/workdrive/rune-code")
|
||||
|
||||
|
||||
def rune_home() -> str:
|
||||
"""Fail-loud Go delegation (see module header)."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "rune_home"}])[0]
|
||||
return _rune_home_python()
|
||||
|
||||
|
||||
def _rune_home_python() -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
"""The operator home (env override, ~/.rune default)."""
|
||||
return os.environ.get("RUNE_HOME", os.path.expanduser("~/.rune"))
|
||||
|
||||
|
||||
def harness_scripts() -> str:
|
||||
"""Fail-loud Go delegation (see module header)."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "harness_scripts"}])[0]
|
||||
return _harness_scripts_python()
|
||||
|
||||
|
||||
def _harness_scripts_python() -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
"""Harness scripts dir (env HARNESS_SCRIPTS override)."""
|
||||
return os.environ.get(
|
||||
"HARNESS_SCRIPTS", "/home/nick/.config/opencode/scripts")
|
||||
return os.environ.get("HARNESS_SCRIPTS", "/home/nick/.config/opencode/scripts")
|
||||
|
||||
|
||||
def wait_resume_dir() -> str:
|
||||
"""Fail-loud Go delegation (see module header)."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval([{"op": "wait_resume_dir"}])[0]
|
||||
return _wait_resume_dir_python()
|
||||
|
||||
|
||||
def _wait_resume_dir_python() -> str:
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
"""The wait-resume bridge dir (env RUNE_WR_CODE override)."""
|
||||
return os.environ.get(
|
||||
"RUNE_WR_CODE", "/home/nick/.local/share/mimo-wait-resume")
|
||||
return os.environ.get("RUNE_WR_CODE", "/home/nick/.local/share/mimo-wait-resume")
|
||||
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
|
||||
|
||||
This is not an interface. This is how I see, speak, and interact with the world.
|
||||
|
||||
Version: v0.351.0
|
||||
Version: v0.358.1
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -2047,7 +2047,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.351.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.358.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+73
-1
@@ -34,10 +34,64 @@ Usage:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail-loud Go delegation (REBASE-COMPLETION, #2587 wave): handle_hook and
|
||||
# the plugin manifest resolve through the Go engine (src/go/voicebox,
|
||||
# `eval <ops.json>` surface). Python body kept as oracle/escape hatch
|
||||
# (RUNE_VOICEBOX_ENGINE=python); RUNE_VOICEBOX_BIN overrides the binary.
|
||||
# Missing binary or engine error raises RuntimeError — no silent fallback.
|
||||
# The speak/listen/talk audio loop stays Python (pynput/venv-bound reactive
|
||||
# surface — the Go port records the same boundary).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _go_engine_enabled() -> bool:
|
||||
return os.environ.get("RUNE_VOICEBOX_ENGINE", "go") != "python"
|
||||
|
||||
|
||||
def _go_engine_bin() -> str:
|
||||
override = os.environ.get("RUNE_VOICEBOX_BIN")
|
||||
if override:
|
||||
return override
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
return os.path.join(root, "src", "go", "voicebox", "voicebox-bin")
|
||||
|
||||
|
||||
def _go_eval(ops: list) -> list:
|
||||
bin_path = _go_engine_bin()
|
||||
if not os.path.exists(bin_path):
|
||||
raise RuntimeError(
|
||||
f"voicebox go engine binary missing: {bin_path} (set RUNE_VOICEBOX_ENGINE=python for the legacy path)"
|
||||
)
|
||||
fd, ops_path = tempfile.mkstemp(suffix=".json", prefix="vb-ops-")
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
json.dump(ops, fh)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bin_path, "eval", ops_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"voicebox go engine failed: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"voicebox go engine exit {proc.returncode}: {proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(ops_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
VENV = "/tmp/opencode/voice-venv/bin/python3"
|
||||
SAY = os.path.join(HERE, "voicebox_say.py")
|
||||
@@ -144,7 +198,25 @@ def main():
|
||||
# Hook handler — called by the microkernel when an event fires.
|
||||
# ---------------------------------------------------------------------------
|
||||
def handle_hook(event, data=None):
|
||||
"""Generic hook handler. Override in specific scripts for custom behavior."""
|
||||
"""Fail-loud Go delegation (see module header). The engine applies its
|
||||
documented improvement: an EMPTY event returns status "error" (the
|
||||
oracle silently returned ok for a no-op) — the improvement surfaces
|
||||
through the delegation, deliberately diverging from the oracle."""
|
||||
if _go_engine_enabled():
|
||||
return _go_eval(
|
||||
[
|
||||
{
|
||||
"op": "handle_hook",
|
||||
"event": event,
|
||||
"data": data or {},
|
||||
}
|
||||
]
|
||||
)[0]
|
||||
return _handle_hook_python(event, data)
|
||||
|
||||
|
||||
def _handle_hook_python(event, data=None):
|
||||
"""Oracle body (kept as escape/oracle; see module header)."""
|
||||
return {"status": "ok", "event": event, "handler": "default"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"checkpoints": [
|
||||
{
|
||||
"at": "2026-10-25T00:44:54Z",
|
||||
"evidence": "predicted-frontier",
|
||||
"note": "{\"level0_direct\": [], \"level1_obligations\": [], \"contracts\": [], \"level2_verification\": []}",
|
||||
"phase": "draft"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-25T00:45:17Z",
|
||||
"evidence": "user:D56 directive 2026-10-24 (Gitea #2580 + #2581)",
|
||||
"note": "",
|
||||
"phase": "approved"
|
||||
},
|
||||
{
|
||||
"at": "2026-10-25T00:45:39Z",
|
||||
"evidence": "builder:wave-d56-governor",
|
||||
"note": "",
|
||||
"phase": "building"
|
||||
}
|
||||
],
|
||||
"evidence_refs": [
|
||||
"contract-created:ADMIT-wave-d56-governor"
|
||||
],
|
||||
"ext": {
|
||||
"activation": {
|
||||
"activated": true,
|
||||
"builder": "wave-d56-governor",
|
||||
"evidence": [
|
||||
"affected surface known: ['src/go/rune-core/', 'src/go/dispatch-classifier/']",
|
||||
"file mention binds scoped target",
|
||||
"scoping words bind to known target: ['current']",
|
||||
"smallness markers: ['patch']"
|
||||
],
|
||||
"lifecycle": "bounded",
|
||||
"mode": "LOCAL_CHANGE",
|
||||
"note": "bounded governance via existing authority \u2014 no Project Scaffold bootstrap",
|
||||
"project_authority": "rune_internal",
|
||||
"target_project": "rune"
|
||||
},
|
||||
"affected_paths": [
|
||||
"scripts/admit_wave_d56_governor.py",
|
||||
"src/go/dispatch-classifier/",
|
||||
"src/go/rune-core/",
|
||||
"tui/bridge/",
|
||||
"tui/cmd/rune-tui/",
|
||||
"tui/lanes/",
|
||||
"tui/runtime/lib/"
|
||||
],
|
||||
"affected_regions": [],
|
||||
"affected_ui_surfaces": [],
|
||||
"expected_runtime_effects": [
|
||||
"the governor samples live tok/s per provider from real turns and persists the rolling model to the hot state, routes cloud-first through healthy providers only (429-throttled refused), the dispatch classifier's PARALLEL verdicts carry the measured lane capacity with the thin pipe stated honestly, and the info bar shows the measured tok/s after the context readout with the sub-agents section showing the governor's state"
|
||||
],
|
||||
"predicted_frontier": {
|
||||
"change_kind": "implementation_change",
|
||||
"contextual_not_obligations": [],
|
||||
"contracts": [],
|
||||
"level0_direct": [],
|
||||
"level1_obligations": [],
|
||||
"level2_verification": [],
|
||||
"max_level": 2,
|
||||
"traversal_scope": "spine-structural-only"
|
||||
},
|
||||
"scope_provenance": {
|
||||
"caller_added": [],
|
||||
"generated_from_affected_surface": [
|
||||
"scripts/admit_wave_d56_governor.py",
|
||||
"src/go/dispatch-classifier/",
|
||||
"src/go/rune-core/",
|
||||
"tui/bridge/",
|
||||
"tui/cmd/rune-tui/",
|
||||
"tui/lanes/",
|
||||
"tui/runtime/lib/"
|
||||
]
|
||||
},
|
||||
"task_ref": "wave-d56-governor",
|
||||
"unresolved_surfaces": [
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "scripts/admit_wave_d56_governor.py"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "src/go/dispatch-classifier/"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "src/go/rune-core/"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "tui/bridge/"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "tui/cmd/rune-tui/"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "tui/lanes/"
|
||||
},
|
||||
{
|
||||
"kind": "source_path",
|
||||
"reason": "no authority records this path as an entity (path remains file-scope only)",
|
||||
"surface": "tui/runtime/lib/"
|
||||
}
|
||||
],
|
||||
"workflow_config_surfaces": []
|
||||
},
|
||||
"id": "contract:ADMIT-wave-d56-governor",
|
||||
"identities": {
|
||||
"acceptance": [
|
||||
"bounded repair verified: land the D56 wave (Gitea #2580 + #2581, owner doctrine rulings 2026-10-24: CLOUD-FIRST routing \u2014 cloud lanes fill FIRST,"
|
||||
],
|
||||
"affected_entities": [],
|
||||
"expected_consumers": [],
|
||||
"expected_new_entities": [
|
||||
"the D56 wave report (reports/)",
|
||||
"the governor hot-state shape (~/.rune/hot/provider_throughput.json)",
|
||||
"the red-first governor/capacity/readout tests"
|
||||
],
|
||||
"gap_evidence": "smallness markers: ['patch']; scoping words bind to known target: ['current']; file mention binds scoped target; affected surface known: ['src/go/rune-core/', 'src/go/dispatch-classifier/']",
|
||||
"migration_obligations": [],
|
||||
"need": "land the D56 wave (Gitea #2580 + #2581, owner doctrine rulings 2026-10-24: CLOUD-FIRST routing \u2014 cloud lanes fill FIRST, local absorbs only overflow; NO CONSENT on cloud models \u2014 configured provider = spend freely, zero prompts; routing scope = factory lane assignment, interactive chat routing stays the owner's): (1) TELEMETRY \u2014 sample live tok/s per provider from production data (the engine's real turn accounting; output-token counts from the provider wires where published, honest token-estimate fallback otherwise); rolling per-provider samples persisted via the hot-state pattern (~/.rune/hot/provider_throughput.json, atomic temp+rename); (2) SATURATION MODEL \u2014 a small rolling (provider, lanes) EMA curve learned ONLY from samples, no synthetic benchmarks; the lane cap is where the marginal tok/s gain clears the floor (RUNE_GOVERNOR_CAP_FLOOR, default 5); (3) CONNECTION-HEALTH \u2014 the governor polls the registry's enabled providers live (reachable, authenticated, tok/s, error rate, 429 throttle windows) and routes only to providers actually alive; (4) LANE POOLS + CLOUD-FIRST \u2014 per-provider lane pools, cloud pools fill first, least-loaded within a tier, throttled providers refused (a 429 ceiling is worse than not spawning); (5) CLASSIFIER INTEGRATION \u2014 dispatch-classifier gains the governor's cap: PARALLEL verdicts become throughput-aware ('PARALLEL (N lanes free at current tok/s)'), the thin pipe says so, no false fan-out; registry/lock semantics unchanged; (6) INFO BAR READOUT \u2014 tok/s after the context readout ('context 4.2k/32k (13%) \u00b7 38 tok/s') fed from /v1/status's new governor section through both bridge seams; the sub-agents section shows the governor's state row (lanes in use, measured tok/s, cap, '\u2014' honest gap); (7) HONESTY \u2014 a thin pipe is stated, never dressed up",
|
||||
"next_action": "complete implementation, then verify",
|
||||
"non_goals": [],
|
||||
"origin": "worker",
|
||||
"regression_obligations": [],
|
||||
"requirement": "headless admission (classified: LOCAL_CHANGE)",
|
||||
"research_refs": [
|
||||
"src/go/dispatch-classifier/capacity.go",
|
||||
"src/go/dispatch-classifier/capacity_test.go",
|
||||
"src/go/rune-core/internal/runecore/engine.go",
|
||||
"src/go/rune-core/internal/runecore/governor.go",
|
||||
"src/go/rune-core/internal/runecore/governor_test.go",
|
||||
"src/go/rune-core/internal/runecore/server.go",
|
||||
"tui/bridge/d56_governor_test.go",
|
||||
"tui/cmd/rune-tui/d56_governor_test.go",
|
||||
"tui/cmd/rune-tui/main.go",
|
||||
"tui/runtime/lib/lib.go"
|
||||
],
|
||||
"resolution": "",
|
||||
"rollback_ref": "git revert",
|
||||
"scope": [
|
||||
"scripts/admit_wave_d56_governor.py",
|
||||
"src/go/dispatch-classifier/",
|
||||
"src/go/rune-core/",
|
||||
"tui/bridge/",
|
||||
"tui/cmd/rune-tui/",
|
||||
"tui/lanes/",
|
||||
"tui/runtime/lib/"
|
||||
],
|
||||
"toolchain": {},
|
||||
"verification_obligations": [
|
||||
"go test -race -short green on rune-core, dispatch-classifier and tui (hermetic PTY/HTTP tests)",
|
||||
"readout proof: the info-bar test asserts 'context ... (13%) \u00b7 38 tok/s' byte-shape against a fake serve",
|
||||
"vet/gofmt clean on touched modules"
|
||||
]
|
||||
},
|
||||
"kind": "construction_contract",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-10-25T00:45:39Z",
|
||||
"phase": "building",
|
||||
"status": "open",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — abstraction.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface (list_beliefs,
|
||||
list_rules, list_skills, pipeline_stats, handle_hook) against the
|
||||
Python oracle bodies on the SAME abstraction.db. A mutation of an
|
||||
oracle body must flip the battery. Also pins the facade contract:
|
||||
fail-loud RuntimeError on a missing binary; RUNE_ABSTRACTION_ENGINE
|
||||
=python escape; cross-engine coherence.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import abstraction as abs_mod # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mem_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(abs_mod, "DB_PATH", str(tmp_path / "abstraction.db"))
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
def _seed_db(mem_dir):
|
||||
# Let the ENGINE create the full schema first (its Connect migrates);
|
||||
# a battery-local minimal schema would block the IF-NOT-EXISTS columns.
|
||||
abs_mod.pipeline_stats()
|
||||
db = sqlite3.connect(abs_mod.DB_PATH)
|
||||
db.execute("INSERT INTO beliefs (claim, confidence, formed_at) VALUES ('b1', 0.9, '2026-01-01T00:00:00+00:00')")
|
||||
db.execute("INSERT INTO rules (pattern, confidence, created_at) VALUES ('r1', 0.8, '2026-01-01T00:00:00+00:00')")
|
||||
db.execute("INSERT INTO skills (name, success_count, created_at) VALUES ('s1', 3, '2026-01-01T00:00:00+00:00')")
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
# -- reads go vs oracle --------------------------------------------------------
|
||||
|
||||
|
||||
def test_list_beliefs_go_vs_oracle(mem_dir):
|
||||
_seed_db(mem_dir)
|
||||
got = abs_mod.list_beliefs()
|
||||
oracle = abs_mod._list_beliefs_python()
|
||||
assert got == oracle and len(got) == 1
|
||||
assert got[0]["claim"] == "b1"
|
||||
|
||||
|
||||
def test_list_rules_go_vs_oracle(mem_dir):
|
||||
_seed_db(mem_dir)
|
||||
assert abs_mod.list_rules() == abs_mod._list_rules_python()
|
||||
assert abs_mod.list_rules()[0]["pattern"] == "r1"
|
||||
|
||||
|
||||
def test_list_skills_go_vs_oracle(mem_dir):
|
||||
_seed_db(mem_dir)
|
||||
assert abs_mod.list_skills() == abs_mod._list_skills_python()
|
||||
assert abs_mod.list_skills()[0]["name"] == "s1"
|
||||
|
||||
|
||||
def test_pipeline_stats_go_vs_oracle(mem_dir):
|
||||
_seed_db(mem_dir)
|
||||
got = abs_mod.pipeline_stats()
|
||||
oracle = abs_mod._pipeline_stats_python()
|
||||
assert got == oracle
|
||||
assert got["beliefs"] == {"total": 1, "active": 1}
|
||||
assert got["facts"] == {"total": 0, "active": 0}
|
||||
|
||||
|
||||
def test_handle_hook_go_vs_oracle(mem_dir):
|
||||
got = abs_mod.handle_hook("on_session_end", {"k": 1})
|
||||
oracle = abs_mod._handle_hook_python("on_session_end", {"k": 1})
|
||||
assert got == oracle == {"status": "ok", "event": "on_session_end", "handler": "abstraction"}
|
||||
|
||||
|
||||
# -- cross-engine coherence ------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_write_read_same_db(mem_dir):
|
||||
# the engine reads exactly what the oracle wrote (same db file)
|
||||
_seed_db(mem_dir)
|
||||
os.environ["RUNE_ABSTRACTION_ENGINE"] = "python"
|
||||
try:
|
||||
oracle_rows = abs_mod.list_beliefs()
|
||||
finally:
|
||||
os.environ.pop("RUNE_ABSTRACTION_ENGINE")
|
||||
assert abs_mod.list_beliefs() == oracle_rows
|
||||
|
||||
|
||||
# -- facade contract ---------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle(mem_dir):
|
||||
os.environ["RUNE_ABSTRACTION_ENGINE"] = "python"
|
||||
try:
|
||||
assert abs_mod.pipeline_stats()["facts"]["total"] == 0
|
||||
finally:
|
||||
os.environ.pop("RUNE_ABSTRACTION_ENGINE")
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(mem_dir, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_ABSTRACTION_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
abs_mod.list_beliefs()
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
abs_mod._go_eval([{"op": "definitely_not_an_op", "memory_dir": "/tmp"}])
|
||||
@@ -0,0 +1,100 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — auto_context.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface
|
||||
(build_auto_context, load_active_tasks) against the Python oracle
|
||||
bodies. A mutation of an oracle body must flip the battery. Also pins
|
||||
the facade contract: fail-loud RuntimeError on a missing binary;
|
||||
RUNE_AUTO_CONTEXT_ENGINE=python escape.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import auto_context as ac_mod # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mem_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
tasks = tmp_path / "tasks"
|
||||
tasks.mkdir()
|
||||
(tasks / "persisted.json").write_text(
|
||||
'{"tasks": [{"status": "open", "name": "t1"},'
|
||||
' {"status": "in_progress", "name": "t2"},'
|
||||
' {"status": "done", "name": "t3"}]}'
|
||||
)
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
# -- build_auto_context ----------------------------------------------------
|
||||
|
||||
|
||||
def test_build_auto_context_go_vs_oracle(mem_dir):
|
||||
got = ac_mod.build_auto_context("hello world")
|
||||
oracle = ac_mod._build_auto_context_python("hello world")
|
||||
assert got.pop("timestamp") == oracle.pop("timestamp") or True
|
||||
got.pop("timestamp", None)
|
||||
oracle.pop("timestamp", None)
|
||||
assert got == oracle
|
||||
assert got["message_preview"] == "hello world"
|
||||
assert got["active_tasks"] == [{"name": "t1", "status": "open"}, {"name": "t2", "status": "in_progress"}]
|
||||
# warm-router subprocess runs identically on both engines (same script)
|
||||
assert got["warm_neurons"] == oracle["warm_neurons"]
|
||||
|
||||
|
||||
def test_build_auto_context_preview_truncated(mem_dir):
|
||||
long_msg = "x" * 500
|
||||
got = ac_mod.build_auto_context(long_msg)
|
||||
assert len(got["message_preview"]) == 200
|
||||
|
||||
|
||||
def test_build_auto_context_empty_message(mem_dir):
|
||||
got = ac_mod.build_auto_context("")
|
||||
oracle = ac_mod._build_auto_context_python("")
|
||||
got.pop("timestamp", None)
|
||||
oracle.pop("timestamp", None)
|
||||
assert got == oracle
|
||||
|
||||
|
||||
# -- load_active_tasks ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_load_active_tasks_go_vs_oracle(mem_dir):
|
||||
got = ac_mod.load_active_tasks()
|
||||
oracle = ac_mod._load_active_tasks_python()
|
||||
assert got == oracle
|
||||
assert [t["name"] for t in got] == ["t1", "t2"]
|
||||
|
||||
|
||||
def test_load_active_tasks_missing_file(mem_dir):
|
||||
os.remove(os.path.join(mem_dir, "tasks", "persisted.json"))
|
||||
assert ac_mod.load_active_tasks() == []
|
||||
assert ac_mod._load_active_tasks_python() == []
|
||||
|
||||
|
||||
# -- facade contract --------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle(mem_dir):
|
||||
os.environ["RUNE_AUTO_CONTEXT_ENGINE"] = "python"
|
||||
try:
|
||||
assert ac_mod.load_active_tasks() == ac_mod.load_active_tasks()
|
||||
tasks = ac_mod.load_active_tasks()
|
||||
assert [t["name"] for t in tasks] == ["t1", "t2"]
|
||||
finally:
|
||||
os.environ.pop("RUNE_AUTO_CONTEXT_ENGINE", None)
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(mem_dir, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_AUTO_CONTEXT_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
ac_mod.build_auto_context("hi")
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
ac_mod._go_eval([{"op": "definitely_not_an_op", "memory_dir": "/tmp"}])
|
||||
@@ -0,0 +1,151 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — cache.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface (get_cache,
|
||||
put_cache, invalidate, invalidate_scope, invalidate_all, cache_stats,
|
||||
handle_hook) against the Python oracle bodies on the SAME cache.db. A
|
||||
mutation of an oracle body must flip the battery. Also pins the facade
|
||||
contract: fail-loud RuntimeError on a missing binary; RUNE_CACHE_ENGINE
|
||||
=python escape; cross-engine coherence (engine-written rows are
|
||||
oracle-readable and vice versa).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import cache as cache_mod # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mem_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
# The oracle bodies read the import-time DB_PATH constant; retarget it
|
||||
# to the test memory dir so both engines address the same cache.db.
|
||||
monkeypatch.setattr(cache_mod, "DB_PATH", str(tmp_path / "cache.db"))
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
def _oracle(mem_dir, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CACHE_ENGINE", "python")
|
||||
return cache_mod
|
||||
|
||||
|
||||
def _put(cache, key, value, **kw):
|
||||
return cache.put_cache(key, value, **kw)
|
||||
|
||||
|
||||
# -- put / get roundtrip -----------------------------------------------------
|
||||
|
||||
|
||||
def test_put_get_roundtrip_go_vs_oracle(mem_dir, monkeypatch):
|
||||
got = _put(cache_mod, "k1", "value one")
|
||||
monkeypatch.setenv("RUNE_CACHE_ENGINE", "python")
|
||||
oracle_put = cache_mod._put_cache_python("k1", "value one")
|
||||
oracle_get = cache_mod._get_cache_python("k1")
|
||||
monkeypatch.delenv("RUNE_CACHE_ENGINE")
|
||||
engine_get = cache_mod.get_cache("k1")
|
||||
assert got == oracle_put == {"stored": "k1", "content_hash": got["content_hash"], "scope": "general"}
|
||||
assert engine_get["hit"] is True and oracle_get["hit"] is True
|
||||
assert engine_get["full"] == oracle_get["full"] == "value one"
|
||||
assert engine_get["content_hash"] == oracle_get["content_hash"]
|
||||
|
||||
|
||||
def test_miss_shape_go_vs_oracle(mem_dir):
|
||||
assert cache_mod.get_cache("absent-key") == {"hit": False, "key": "absent-key"}
|
||||
assert cache_mod._get_cache_python("absent-key") == {"hit": False, "key": "absent-key"}
|
||||
|
||||
|
||||
def test_put_scope_and_source_hash_go_vs_oracle(mem_dir):
|
||||
got = _put(cache_mod, "k2", "v2", scope="research", source_hash="abc")
|
||||
oracle = cache_mod._put_cache_python("k2", "v2", scope="research", source_hash="abc")
|
||||
assert got == oracle
|
||||
row = cache_mod.get_cache("k2")
|
||||
assert row["scope"] == "research"
|
||||
|
||||
|
||||
# -- invalidation ------------------------------------------------------------
|
||||
|
||||
|
||||
def test_invalidate_go_vs_oracle(mem_dir):
|
||||
_put(cache_mod, "k3", "v3")
|
||||
got = cache_mod.invalidate("k3", "gone")
|
||||
assert got["invalidated"] == "k3"
|
||||
assert cache_mod.get_cache("k3")["hit"] is False
|
||||
|
||||
|
||||
def test_invalidate_scope_go_vs_oracle(mem_dir):
|
||||
_put(cache_mod, "a1", "v", scope="s1")
|
||||
_put(cache_mod, "a2", "v", scope="s1")
|
||||
_put(cache_mod, "b1", "v", scope="s2")
|
||||
got = cache_mod.invalidate_scope("s1")
|
||||
assert got["invalidated_scope"] == "s1" and got["count"] == 2
|
||||
assert cache_mod.get_cache("a1")["hit"] is False
|
||||
assert cache_mod.get_cache("a2")["hit"] is False
|
||||
assert cache_mod.get_cache("b1")["hit"] is True
|
||||
|
||||
|
||||
def test_invalidate_all_go_vs_oracle(mem_dir):
|
||||
_put(cache_mod, "x1", "v")
|
||||
_put(cache_mod, "x2", "v")
|
||||
got = cache_mod.invalidate_all("reset")
|
||||
assert got["invalidated_all"] == 2
|
||||
assert cache_mod.get_cache("x1")["hit"] is False
|
||||
assert cache_mod.get_cache("x2")["hit"] is False
|
||||
|
||||
|
||||
# -- stats + hook --------------------------------------------------------------
|
||||
|
||||
|
||||
def test_cache_stats_go_vs_oracle(mem_dir):
|
||||
_put(cache_mod, "s1", "v")
|
||||
_put(cache_mod, "s2", "v", scope="other")
|
||||
stats = cache_mod.cache_stats()
|
||||
oracle = cache_mod._cache_stats_python()
|
||||
assert stats["active"] == oracle["active"] == 2
|
||||
assert stats["scopes"] == oracle["scopes"]
|
||||
|
||||
|
||||
def test_handle_hook_go_vs_oracle(mem_dir):
|
||||
got = cache_mod.handle_hook("memory.write", {"key": "h1"})
|
||||
oracle = cache_mod._handle_hook_python("memory.write", {"key": "h1"})
|
||||
assert got["handler"] == oracle["handler"] == "cache"
|
||||
assert got["status"] == oracle["status"] == "ok"
|
||||
|
||||
|
||||
# -- cross-engine coherence -----------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_write_oracle_read(mem_dir, monkeypatch):
|
||||
_put(cache_mod, "cross1", "via engine")
|
||||
monkeypatch.setenv("RUNE_CACHE_ENGINE", "python")
|
||||
assert cache_mod.get_cache("cross1")["full"] == "via engine"
|
||||
|
||||
|
||||
def test_oracle_write_engine_read(mem_dir, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CACHE_ENGINE", "python")
|
||||
cache_mod.put_cache("cross2", "via oracle")
|
||||
monkeypatch.delenv("RUNE_CACHE_ENGINE")
|
||||
assert cache_mod.get_cache("cross2")["full"] == "via oracle"
|
||||
|
||||
|
||||
# -- facade contract -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle(mem_dir, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_CACHE_ENGINE", "python")
|
||||
r = cache_mod.put_cache("esc", "v")
|
||||
assert r["stored"] == "esc"
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(mem_dir, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("RUNE_CACHE_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
cache_mod.get_cache("k")
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
cache_mod._go_eval([{"op": "definitely_not_an_op", "memory_dir": "/tmp"}])
|
||||
@@ -0,0 +1,147 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — message_queue.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface against the
|
||||
Python oracle bodies on the SAME queue.json. A mutation of an oracle
|
||||
body must flip the battery. Also pins the facade contract: fail-loud
|
||||
RuntimeError on a missing binary; RUNE_MESSAGE_QUEUE_ENGINE=python
|
||||
escape; cross-engine coherence both directions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import message_queue as mq_mod # noqa: E402
|
||||
from rune.message_queue import MessageQueue, QueuedMessage # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def queue_dir(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(mq_mod, "QUEUE_DIR", str(tmp_path / "mq"))
|
||||
monkeypatch.setattr(mq_mod, "QUEUE_FILE", str(tmp_path / "mq" / "queue.json"))
|
||||
return str(tmp_path / "mq")
|
||||
|
||||
|
||||
def _engine(queue_dir):
|
||||
return MessageQueue()
|
||||
|
||||
|
||||
def _oracle(queue_dir):
|
||||
os.environ["RUNE_MESSAGE_QUEUE_ENGINE"] = "python"
|
||||
try:
|
||||
return MessageQueue()
|
||||
finally:
|
||||
os.environ.pop("RUNE_MESSAGE_QUEUE_ENGINE", None)
|
||||
|
||||
|
||||
# -- detection ----------------------------------------------------------------
|
||||
|
||||
DETECT_CASES = [
|
||||
"add a new feature to the parser",
|
||||
"also fix the bug in auth",
|
||||
"and can you update the docs",
|
||||
"the weather is nice today",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("message", DETECT_CASES)
|
||||
def test_detect_task_addition_go_vs_oracle(message):
|
||||
is_t, conf, signals = MessageQueue().detect_task_addition(message)
|
||||
o_is, o_conf, o_signals = MessageQueue()._detect_task_addition_python(message)
|
||||
assert (is_t, round(conf, 6), signals) == (o_is, round(o_conf, 6), o_signals)
|
||||
|
||||
|
||||
# -- add / pending / approved / status ------------------------------------------
|
||||
|
||||
|
||||
def test_add_message_go_vs_oracle(queue_dir):
|
||||
got = _engine(queue_dir).add_message("add a feature", "chat")
|
||||
oracle = _oracle(queue_dir).add_message("add a feature", "chat")
|
||||
assert got.message == oracle.message == "add a feature"
|
||||
assert got.status == oracle.status == "pending"
|
||||
assert got.channel == oracle.channel == "chat"
|
||||
|
||||
|
||||
def test_pending_approved_flow_go_vs_oracle(queue_dir):
|
||||
q = _engine(queue_dir)
|
||||
m1 = q.add_message("add one", "chat")
|
||||
q.add_message("add two", "chat")
|
||||
assert len(q.get_pending()) == 2
|
||||
assert q.approve(m1.id) is True
|
||||
assert len(q.get_pending()) == 1
|
||||
assert len(q.get_approved()) == 1
|
||||
st = q.get_status()
|
||||
assert st == {"total": 2, "pending": 1, "approved": 1, "executed": 0}
|
||||
assert q.mark_executed(m1.id) is True
|
||||
assert q.get_status()["executed"] == 1
|
||||
|
||||
|
||||
def test_reject_go_vs_oracle(queue_dir):
|
||||
q = _engine(queue_dir)
|
||||
m = q.add_message("add x", "chat")
|
||||
assert q.reject(m.id) is True
|
||||
st = q.get_status()
|
||||
assert st["pending"] == 0
|
||||
oracle_q = _oracle(queue_dir)
|
||||
om = oracle_q.add_message("add x", "chat")
|
||||
oracle_q.reject(om.id)
|
||||
assert q.get_status() == oracle_q.get_status()
|
||||
|
||||
|
||||
def test_approve_all_go_vs_oracle(queue_dir):
|
||||
q = _engine(queue_dir)
|
||||
q.add_message("a1", "chat")
|
||||
q.add_message("a2", "chat")
|
||||
assert q.approve_all() == 2
|
||||
assert len(q.get_approved()) == 2
|
||||
|
||||
|
||||
def test_clear_go_vs_oracle(queue_dir):
|
||||
q = _engine(queue_dir)
|
||||
q.add_message("a", "chat")
|
||||
q.clear()
|
||||
assert q.get_status()["total"] == 0
|
||||
|
||||
|
||||
# -- cross-engine coherence --------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_write_oracle_read(queue_dir):
|
||||
q = _engine(queue_dir)
|
||||
m = q.add_message("cross", "chat")
|
||||
mo = _oracle(queue_dir)
|
||||
assert any(x.id == m.id for x in mo.get_pending())
|
||||
|
||||
|
||||
def test_oracle_write_engine_read(queue_dir):
|
||||
mo = _oracle(queue_dir)
|
||||
m = mo.add_message("cross back", "chat")
|
||||
q = _engine(queue_dir)
|
||||
assert any(x.id == m.id for x in q.get_pending())
|
||||
|
||||
|
||||
# -- facade contract ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle(queue_dir):
|
||||
os.environ["RUNE_MESSAGE_QUEUE_ENGINE"] = "python"
|
||||
try:
|
||||
q = MessageQueue()
|
||||
m = q.add_message("oracle path", "chat")
|
||||
assert m.status == "pending"
|
||||
finally:
|
||||
os.environ.pop("RUNE_MESSAGE_QUEUE_ENGINE", None)
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(tmp_path, queue_dir, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_MESSAGE_QUEUE_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
MessageQueue().add_message("x", "chat")
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
mq_mod._go_eval([{"op": "definitely_not_an_op", "queue_dir": "/tmp"}])
|
||||
@@ -0,0 +1,84 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — paths.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface (repo_root,
|
||||
crate_dir, deploy_root, rune_home, harness_scripts, wait_resume_dir)
|
||||
against the Python oracle bodies. A mutation of an oracle body must
|
||||
flip the battery. Also pins the facade contract: fail-loud RuntimeError
|
||||
on a missing binary; RUNE_PATHS_ENGINE=python escape.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import paths as paths_mod # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _env_clean(monkeypatch):
|
||||
for var in ("RUNE_DEPLOY_ROOT", "RUNE_HOME", "HARNESS_SCRIPTS", "RUNE_WR_CODE"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
|
||||
# -- resolvers go vs oracle -----------------------------------------------------
|
||||
|
||||
|
||||
def test_repo_root_go_vs_oracle():
|
||||
assert paths_mod.repo_root() == paths_mod._repo_root_python()
|
||||
assert os.path.isdir(paths_mod.repo_root())
|
||||
|
||||
|
||||
def test_crate_dir_go_vs_oracle():
|
||||
got = paths_mod.crate_dir("survivor")
|
||||
oracle = paths_mod._crate_dir_python("survivor")
|
||||
assert got == oracle
|
||||
assert got.endswith(os.path.join("src", "rust", "survivor"))
|
||||
|
||||
|
||||
def test_deploy_root_go_vs_oracle(monkeypatch):
|
||||
monkeypatch.setenv("RUNE_DEPLOY_ROOT", "/tmp/deploy")
|
||||
assert paths_mod.deploy_root() == paths_mod._deploy_root_python() == "/tmp/deploy"
|
||||
monkeypatch.delenv("RUNE_DEPLOY_ROOT")
|
||||
assert paths_mod.deploy_root() == paths_mod._deploy_root_python()
|
||||
|
||||
|
||||
def test_rune_home_go_vs_oracle(monkeypatch):
|
||||
monkeypatch.setenv("RUNE_HOME", "/tmp/rh")
|
||||
assert paths_mod.rune_home() == paths_mod._rune_home_python() == "/tmp/rh"
|
||||
monkeypatch.delenv("RUNE_HOME")
|
||||
assert paths_mod.rune_home() == paths_mod._rune_home_python()
|
||||
|
||||
|
||||
def test_harness_scripts_go_vs_oracle(monkeypatch):
|
||||
monkeypatch.setenv("HARNESS_SCRIPTS", "/tmp/hs")
|
||||
assert paths_mod.harness_scripts() == paths_mod._harness_scripts_python() == "/tmp/hs"
|
||||
|
||||
|
||||
def test_wait_resume_dir_go_vs_oracle(monkeypatch):
|
||||
monkeypatch.setenv("RUNE_WR_CODE", "/tmp/wr")
|
||||
assert paths_mod.wait_resume_dir() == paths_mod._wait_resume_dir_python() == "/tmp/wr"
|
||||
|
||||
|
||||
# -- facade contract ----------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle():
|
||||
os.environ["RUNE_PATHS_ENGINE"] = "python"
|
||||
try:
|
||||
assert paths_mod.repo_root() == paths_mod.REPO_ROOT
|
||||
finally:
|
||||
os.environ.pop("RUNE_PATHS_ENGINE", None)
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_PATHS_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
paths_mod.repo_root()
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
paths_mod._go_eval([{"op": "definitely_not_an_op"}])
|
||||
@@ -0,0 +1,78 @@
|
||||
"""REBASE-COMPLETION (#2587) — the pins boundary verdict, as tests.
|
||||
|
||||
The audit (#2587) classified pins as "Python-only, no Go side". The tree
|
||||
disagrees on both counts, and the honest classification is neither
|
||||
"port it" nor "wire it":
|
||||
|
||||
1. A Go pins port EXISTS (src/go/pins — batch 58) but carries ONLY a
|
||||
recall kernel over (state, item_id) — no details-blob projection, no
|
||||
unpin, no store. It is a parity artifact, not an authority.
|
||||
|
||||
2. pins is ALREADY delegated: the recall/unpin/list/store-path kernels
|
||||
resolve through the Rust cognition_core (src/rust/cognition_core
|
||||
broker.rs pins_recall/pins_unpin/pins_list/pins_store_path — live,
|
||||
verified by identity_anchors._cc_core probe). The Python bodies below
|
||||
the cc branch are the RUNE_COGNITION_CORE=python fallbacks.
|
||||
|
||||
Wiring a Go delegation would introduce a THIRD engine over the same
|
||||
semantics — violating no-duplicate-authority. The verdict:
|
||||
Python-boundary module, Rust-kernel authority, Go kernel is surplus.
|
||||
|
||||
These tests pin that topology so drift in either direction flips them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import pins as pins_mod # noqa: E402
|
||||
from rune.identity_anchors import _cc_core # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def pins_env(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_MEMORY_DIR", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_verdict_rust_kernel_is_the_authority():
|
||||
"""The cognition_core probe is live: recall/list/unpin resolve through
|
||||
the Rust kernel, not the Python fallbacks, not the Go kernel."""
|
||||
assert _cc_core() is not None
|
||||
broker = _cc_core()
|
||||
for fn in ("pins_recall", "pins_unpin", "pins_list", "pins_store_path"):
|
||||
assert hasattr(broker, fn), fn
|
||||
|
||||
|
||||
def test_verdict_go_kernel_is_surplus_not_wired():
|
||||
"""The Go pins port exists but its recall kernel is a subset (state +
|
||||
item_id only, no details projection). It is NOT the delegation target:
|
||||
no facade references it, and src/rune/pins.py imports no Go engine."""
|
||||
go_dir = os.path.join(os.path.dirname(pins_mod.__file__), "..", "go", "pins")
|
||||
assert os.path.isdir(go_dir), "Go pins port exists"
|
||||
src = open(os.path.join(pins_mod.__file__)).read()
|
||||
assert "RUNE_PINS_ENGINE" not in src, (
|
||||
"a Go pins facade appeared — that would be a THIRD engine over "
|
||||
"semantics the Rust cognition_core already governs; remove it or "
|
||||
"retire the Rust authority first"
|
||||
)
|
||||
|
||||
|
||||
def test_verdict_store_stays_python_canonical(pins_env):
|
||||
"""The pin store is the canonical PinRecord JSON under memory_dir —
|
||||
written through AtomicStore (Python), read by all engines. This is
|
||||
the reactive surface that justifies the Python boundary."""
|
||||
out = pins_mod.pin("topic t", "question q", "measurable outcome")
|
||||
assert out == {"pinned": True, "wing": "pins", "topic": "topic t", "outcome": "measurable outcome"}
|
||||
store = pins_env / "store" / "pins.json"
|
||||
assert store.exists()
|
||||
recs = pins_mod._load()
|
||||
assert recs[0]["kind"] == "pins" or recs[0].get("owner") == "pins"
|
||||
|
||||
|
||||
def test_verdict_empty_outcome_is_not_a_pin(pins_env):
|
||||
with pytest.raises(ValueError, match="measurable outcome"):
|
||||
pins_mod.pin("t", "q", "")
|
||||
@@ -0,0 +1,72 @@
|
||||
"""REBASE-COMPLETION (#2587) parity battery — voicebox.
|
||||
|
||||
Red-first discipline: pins the Go engine's eval surface (handle_hook,
|
||||
manifest) against the Python oracle body. A mutation of the oracle body
|
||||
must flip the battery. Also pins the facade contract: fail-loud
|
||||
RuntimeError on a missing binary; RUNE_VOICEBOX_ENGINE=python escape;
|
||||
the documented empty-event improvement (engine rejects, oracle
|
||||
accepted) surfaces through the delegation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import voicebox as vb_mod # noqa: E402
|
||||
|
||||
|
||||
# -- handle_hook go vs oracle --------------------------------------------------
|
||||
|
||||
|
||||
def test_handle_hook_go_vs_oracle():
|
||||
for event in ("voice.start", "on_session_end", "voicebox.speak"):
|
||||
got = vb_mod.handle_hook(event)
|
||||
oracle = vb_mod._handle_hook_python(event)
|
||||
assert got == oracle == {"status": "ok", "event": event, "handler": "default"}
|
||||
|
||||
|
||||
def test_empty_event_engine_improvement():
|
||||
"""DOCUMENTED DIVERGENCE: the oracle silently returns ok for an empty
|
||||
event (a no-op lie); the engine rejects it. The delegation surfaces
|
||||
the improvement deliberately."""
|
||||
got = vb_mod.handle_hook("")
|
||||
assert got["status"] == "error"
|
||||
assert got["error"] == "empty event"
|
||||
oracle = vb_mod._handle_hook_python("")
|
||||
assert oracle == {"status": "ok", "event": "", "handler": "default"}
|
||||
|
||||
|
||||
def test_manifest_pinned():
|
||||
row = vb_mod._go_eval([{"op": "manifest"}])[0]
|
||||
assert row["name"] == "voicebox"
|
||||
assert row["capabilities"] == ["voice", "write", "voicebox"]
|
||||
assert row["priority"] == 100
|
||||
assert row["auto_activate"] is True
|
||||
# the Python PLUGIN literal is the manifest's oracle twin
|
||||
for key in ("name", "version", "capabilities", "priority"):
|
||||
assert row[key] == vb_mod.PLUGIN[key]
|
||||
|
||||
|
||||
# -- facade contract -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_engine_disabled_uses_oracle():
|
||||
os.environ["RUNE_VOICEBOX_ENGINE"] = "python"
|
||||
try:
|
||||
assert vb_mod.handle_hook("x") == {"status": "ok", "event": "x", "handler": "default"}
|
||||
finally:
|
||||
os.environ.pop("RUNE_VOICEBOX_ENGINE", None)
|
||||
|
||||
|
||||
def test_missing_binary_fails_loud(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("RUNE_VOICEBOX_BIN", str(tmp_path / "nope"))
|
||||
with pytest.raises(RuntimeError, match="binary missing"):
|
||||
vb_mod.handle_hook("x")
|
||||
|
||||
|
||||
def test_bad_op_fails_loud():
|
||||
with pytest.raises(RuntimeError, match="go engine"):
|
||||
vb_mod._go_eval([{"op": "definitely_not_an_op"}])
|
||||
@@ -191,6 +191,12 @@ type Bridge struct {
|
||||
ctxUsed int64
|
||||
ctxLimit int64
|
||||
|
||||
// governor (D56): the latest status round's dispatch-governor
|
||||
// readout — measured tok/s and lanes in use vs the learned cap.
|
||||
govTok float64
|
||||
govLanes int
|
||||
govCap int
|
||||
|
||||
// chatDepth is the chat-channel window depth (messages) — the
|
||||
// scrollback deeper-page knob (depth.go), grow-only, wake-armed.
|
||||
chatDepth int
|
||||
@@ -257,6 +263,7 @@ func (b *Bridge) pollOnce(p *tea.Program) {
|
||||
b.record(pongMsg(b.nextID(), nonce))
|
||||
b.record(snapshotMsg(b.nextID(), body))
|
||||
b.setCtx(body.Context.Used, body.Context.Limit)
|
||||
b.setGovernor(body.Governor.TokensPerSec, body.Governor.LanesActive, body.Governor.LanesCap)
|
||||
|
||||
msgs, err := b.serve.Conversation(ctx, DefaultChannel, convWindow)
|
||||
if err != nil {
|
||||
@@ -373,6 +380,22 @@ func (b *Bridge) Ctx() (used, limit int64) {
|
||||
return b.ctxUsed, b.ctxLimit
|
||||
}
|
||||
|
||||
// setGovernor stores the latest status round's governor readout (D56).
|
||||
func (b *Bridge) setGovernor(tokPerSec float64, lanes, cap int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.govTok, b.govLanes, b.govCap = tokPerSec, lanes, cap
|
||||
}
|
||||
|
||||
// Governor reports the dispatch governor's honest live state: the
|
||||
// measured tok/s (0 = nothing measured yet — the bar renders '—',
|
||||
// never a guess) and the lanes in use against the learned cap.
|
||||
func (b *Bridge) Governor() (tokPerSec float64, lanes, cap int) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.govTok, b.govLanes, b.govCap
|
||||
}
|
||||
|
||||
// Token reports the honest connection state for the identity line.
|
||||
func (b *Bridge) Token() string {
|
||||
b.mu.Lock()
|
||||
|
||||
@@ -43,6 +43,16 @@ type StatusBody struct {
|
||||
Used int64 `json:"used"`
|
||||
Limit int64 `json:"limit"`
|
||||
} `json:"context"`
|
||||
// Governor (D56): the dispatch governor's honest live state —
|
||||
// tok/s measured (0 = nothing measured yet), lanes in use vs cap.
|
||||
Governor GovernorState `json:"governor"`
|
||||
}
|
||||
|
||||
// GovernorState is the /v1/status governor section the TUI renders.
|
||||
type GovernorState struct {
|
||||
TokensPerSec float64 `json:"tokens_per_sec"`
|
||||
LanesActive int `json:"lanes_active"`
|
||||
LanesCap int `json:"lanes_cap"`
|
||||
}
|
||||
|
||||
// ConvMessage is one GET /v1/conversation window entry — the durable
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package bridge
|
||||
|
||||
// d56_governor_test.go — the D56 governor readout's seam laws on BOTH
|
||||
// paths: /v1/status's governor section lands in Governor() (serve),
|
||||
// and the library seam carries the StatusRow governor fields the same
|
||||
// way (the info bar reads both).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"rune.dev/rune/tui/runtime/lib"
|
||||
)
|
||||
|
||||
func TestServeSeamCarriesGovernor(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"identity": "rune", "role": "assistant", "current_task": nil,
|
||||
"context": map[string]int{"used": 1, "limit": 2},
|
||||
"governor": map[string]any{"tokens_per_sec": 38.6, "lanes_active": 2, "lanes_cap": 5},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
b := New(NewServeClient(srv.URL), 5*time.Millisecond)
|
||||
b.Start(nil)
|
||||
t.Cleanup(b.Close)
|
||||
waitFor(t, "governor readout (serve seam)", func() bool {
|
||||
tok, lanes, cap := b.Governor()
|
||||
return int(tok+0.5) == 39 && lanes == 2 && cap == 5
|
||||
})
|
||||
}
|
||||
|
||||
func TestLibrarySeamCarriesGovernor(t *testing.T) {
|
||||
fl := &fakeLib{
|
||||
st: lib.StatusRow{Identity: "rune", GovTokPerSec: 12.5, GovLanes: 1, GovCap: 3},
|
||||
}
|
||||
b := NewLibrary(fl, 5*time.Millisecond)
|
||||
b.Start(nil)
|
||||
t.Cleanup(b.Close)
|
||||
waitFor(t, "governor readout (lib seam)", func() bool {
|
||||
tok, lanes, cap := b.Governor()
|
||||
return int(tok) == 12 && lanes == 1 && cap == 3
|
||||
})
|
||||
}
|
||||
|
||||
func TestGovernorDefaultsHonestZero(t *testing.T) {
|
||||
// a bridge that never polled reports zeros — never a guessed rate
|
||||
b := New(NewServeClient("http://127.0.0.1:1"), time.Hour)
|
||||
tok, lanes, cap := b.Governor()
|
||||
if tok != 0 || lanes != 0 || cap != 0 {
|
||||
t.Fatalf("unpolled bridge governor = %v/%d/%d, want zeros", tok, lanes, cap)
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ func (b *Bridge) pollLibrary(p *tea.Program) {
|
||||
b.record(pongMsg(b.nextID(), nonce))
|
||||
b.record(libSnapshotMsg(b.nextID(), st))
|
||||
b.setCtx(st.CtxUsed, st.CtxLimit)
|
||||
b.setGovernor(st.GovTokPerSec, st.GovLanes, st.GovCap)
|
||||
|
||||
taskRows, _, err := b.libSrc.Tasks(ctx)
|
||||
var ownerOrder []string
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
// d56_governor_test.go — the D56 readout's frozen laws: the info bar
|
||||
// carries the governor's measured tok/s after the context readout
|
||||
// ("context 4.2k/32k (13%) · 38 tok/s"), a governor with nothing
|
||||
// measured renders NOTHING (the bar never guesses), and the
|
||||
// sub-agents section shows the governor's own state row (lanes in
|
||||
// use vs cap, measured tok/s, '—' honest gap). Hermetic: fake serve,
|
||||
// no PTY, no live core.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"rune.dev/rune/tui/bridge"
|
||||
"time"
|
||||
)
|
||||
|
||||
func d56Serve(t *testing.T, governor map[string]any) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/v1/status":
|
||||
body := map[string]any{
|
||||
"identity": "rune", "role": "assistant", "current_task": nil,
|
||||
"active_model": "stub-model-a",
|
||||
"context": map[string]int{"used": 4200, "limit": 32000},
|
||||
}
|
||||
if governor != nil {
|
||||
body["governor"] = governor
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
case "/v1/conversation":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"messages": []any{}})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func d56BootModel(t *testing.T, srv *httptest.Server) model {
|
||||
t.Helper()
|
||||
bootServeBase = srv.URL
|
||||
t.Cleanup(func() { bootServeBase = "" })
|
||||
m := sized(t, initialModel(), 100, 24)
|
||||
if m.rt == nil {
|
||||
t.Fatal("no bridge booted with bootServeBase set")
|
||||
}
|
||||
m.rt.Start(nil)
|
||||
t.Cleanup(m.rt.Close)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) &&
|
||||
m.rt.Token() != bridge.TokenConnected && m.rt.Token() != bridge.TokenFeed {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func d56WaitGovernor(t *testing.T, m model, want float64) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if tok, _, _ := m.rt.Governor(); tok == want {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("governor tok/s never reached %v", want)
|
||||
}
|
||||
|
||||
func TestD56InfoBarTokPerSecAfterContext(t *testing.T) {
|
||||
srv := d56Serve(t, map[string]any{
|
||||
"tokens_per_sec": 38.4, "lanes_active": 2, "lanes_cap": 5})
|
||||
m := d56BootModel(t, srv)
|
||||
d56WaitGovernor(t, m, 38.4)
|
||||
next, _ := m.Update(bridge.EventMsg{})
|
||||
got := next.(model)
|
||||
content := got.View().Content
|
||||
if !strings.Contains(content, "(13%) · 38 tok/s") || !strings.Contains(content, "context 4.") {
|
||||
t.Errorf("info bar missing the context+tok/s readout:\n%s", tailLine(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestD56InfoBarNoMeasurementRendersNothing(t *testing.T) {
|
||||
// a status with NO governor section at all — the bar shows only
|
||||
// the context pair, never a guessed rate
|
||||
srv := d56Serve(t, nil)
|
||||
m := d56BootModel(t, srv)
|
||||
d56WaitGovernor(t, m, 0)
|
||||
next, _ := m.Update(bridge.EventMsg{})
|
||||
got := next.(model)
|
||||
content := got.View().Content
|
||||
if strings.Contains(content, "tok/s") {
|
||||
t.Errorf("info bar invented a tok/s with no measurement:\n%s", tailLine(content))
|
||||
}
|
||||
if !strings.Contains(content, "(13%)") || !strings.Contains(content, "context 4.") {
|
||||
t.Errorf("context readout lost: %s", tailLine(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestD56InfoBarZeroRateIsHonestGap(t *testing.T) {
|
||||
// governor present but tok/s 0 — the '—' honesty, not "0 tok/s"
|
||||
srv := d56Serve(t, map[string]any{
|
||||
"tokens_per_sec": 0, "lanes_active": 1, "lanes_cap": 1})
|
||||
m := d56BootModel(t, srv)
|
||||
d56WaitGovernor(t, m, 0)
|
||||
next, _ := m.Update(bridge.EventMsg{})
|
||||
got := next.(model)
|
||||
if strings.Contains(got.View().Content, "tok/s") {
|
||||
t.Errorf("zero measurement rendered as a rate:\n%s", tailLine(got.View().Content))
|
||||
}
|
||||
}
|
||||
|
||||
// tailLine returns the info bar row (the view's last line).
|
||||
func tailLine(content string) string {
|
||||
lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
|
||||
return lines[len(lines)-1]
|
||||
}
|
||||
@@ -2889,7 +2889,7 @@ func (m model) View() tea.View {
|
||||
// the version (hidden at zero, clickable to open the sub-agent tab),
|
||||
// the compacted active model (D44-A: local truncated name+size+style-
|
||||
// if-applicable+ablation; cloud full), and the context window.
|
||||
fmt.Fprintf(&b, "%s%s%s%s", versionToken(m), wavesToken(m), modelToken(m), ctxToken(m))
|
||||
fmt.Fprintf(&b, "%s%s%s%s%s", versionToken(m), wavesToken(m), modelToken(m), ctxToken(m), tokToken(m))
|
||||
v := tea.NewView(b.String())
|
||||
// ALTERNATE SCREEN LAW (owner ruling 2026-10-19: on exit the TUI
|
||||
// must DISAPPEAR — the terminal restores the pre-boot command line,
|
||||
@@ -2940,6 +2940,22 @@ func ctxToken(m model) string {
|
||||
return fmt.Sprintf(" · context %s/%s (%d%%)", compactTokens(used), compactTokens(limit), ctxPercent(used, limit))
|
||||
}
|
||||
|
||||
// tokToken renders the governor's live tok/s readout (D56): " · 38
|
||||
// tok/s" after the context readout — the measured rate from the
|
||||
// core's governor (production samples only). Idle shows the last
|
||||
// measured value; nothing measured renders nothing — the bar never
|
||||
// guesses.
|
||||
func tokToken(m model) string {
|
||||
if m.rt == nil {
|
||||
return ""
|
||||
}
|
||||
tok, _, _ := m.rt.Governor()
|
||||
if tok <= 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" · %d tok/s", int(tok+0.5))
|
||||
}
|
||||
|
||||
// ctxPercent is the usage percentage: integer 0-100, floor-rounded.
|
||||
func ctxPercent(used, limit int64) int {
|
||||
if limit <= 0 {
|
||||
@@ -3493,6 +3509,19 @@ func (m *model) syncWaves() {
|
||||
// rows carry the journal + observations detail waveRows derived.
|
||||
rows := waveRows(lib.DefaultRepoRoot(), fl)
|
||||
avs := make([]lanes.AgentView, 0, len(rows))
|
||||
// THE GOVERNOR'S ROW (D56 item 6): the section shows the machine's
|
||||
// own decision — lanes in use against the learned cap and the
|
||||
// measured tok/s ('—' when nothing was measured; honesty, not a
|
||||
// guess).
|
||||
if m.rt != nil {
|
||||
tok, usedLanes, cap := m.rt.Governor()
|
||||
rate := "—"
|
||||
if tok > 0 {
|
||||
rate = fmt.Sprintf("%d tok/s", int(tok+0.5))
|
||||
}
|
||||
avs = append(avs, lanes.AgentView{ID: "governor", State: "governor",
|
||||
Title: fmt.Sprintf("%s · lanes %d/%d", rate, usedLanes, cap)})
|
||||
}
|
||||
for _, r := range rows {
|
||||
avs = append(avs, lanes.AgentView{ID: r.ID, Title: r.Title, State: r.State, Detail: r.Detail})
|
||||
}
|
||||
|
||||
+13
-1
@@ -111,6 +111,12 @@ type StatusRow struct {
|
||||
Completed int64
|
||||
CtxUsed int64
|
||||
CtxLimit int64
|
||||
// Governor (D56): the dispatch governor's measured tok/s and lane
|
||||
// state — zeros when the core has nothing measured (the bar shows
|
||||
// '—', never a guess).
|
||||
GovTokPerSec float64
|
||||
GovLanes int
|
||||
GovCap int
|
||||
}
|
||||
|
||||
// ModelRow is one /v1/models entry (the surface the core exposes over
|
||||
@@ -354,13 +360,19 @@ func (r *Repo) Status(ctx context.Context) (StatusRow, error) {
|
||||
Used int64 `json:"used"`
|
||||
Limit int64 `json:"limit"`
|
||||
} `json:"context"`
|
||||
Governor struct {
|
||||
TokensPerSec float64 `json:"tokens_per_sec"`
|
||||
LanesActive int `json:"lanes_active"`
|
||||
LanesCap int `json:"lanes_cap"`
|
||||
} `json:"governor"`
|
||||
}
|
||||
if err := json.NewDecoder(body).Decode(&parsed); err != nil {
|
||||
return StatusRow{}, err
|
||||
}
|
||||
return StatusRow{Identity: parsed.Identity, Role: parsed.Role, ActiveMod: parsed.ActiveModel,
|
||||
Pending: parsed.TasksPending, Running: parsed.TasksRunning, Completed: parsed.TasksCompleted,
|
||||
CtxUsed: parsed.Context.Used, CtxLimit: parsed.Context.Limit}, nil
|
||||
CtxUsed: parsed.Context.Used, CtxLimit: parsed.Context.Limit,
|
||||
GovTokPerSec: parsed.Governor.TokensPerSec, GovLanes: parsed.Governor.LanesActive, GovCap: parsed.Governor.LanesCap}, nil
|
||||
}
|
||||
|
||||
// Tasks reads GET /v1/tasks — the task-state checkpoint.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "abstraction",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-25",
|
||||
"parity_basis": "src/go/abstraction truth.json replay (go test -race -short green) + tests/test_abstraction_go_parity.py (9 passed, both engines against the SAME abstraction.db)",
|
||||
"go_engine": {
|
||||
"path": "src/go/abstraction",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — list_beliefs, list_rules, list_skills, pipeline_stats, handle_hook, extract_facts over the truth-tested library; ops carry memory_dir so the facade addresses any abstraction.db) | CLI + replay",
|
||||
"delegation": "src/rune/abstraction.py: list_beliefs, list_rules, list_skills, pipeline_stats, handle_hook resolve through the Go binary (cross-engine coherence pinned); escape hatch RUNE_ABSTRACTION_ENGINE=python; binary override RUNE_ABSTRACTION_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "src/rune/memory_consolidation.py, memory_selector.py, rune_tui.py",
|
||||
"switched_how": "abstraction read/report call sites resolve through the Go engine at runtime",
|
||||
"receipt": "tests/test_delegation_pinned_paths.py green (4); battery 9 passed",
|
||||
"blocks_full_switchover": ["extract_facts, synthesize_observations, run_lifecycle stay Python this wave: they embed the llm_call (Ollama /api/generate, LOCAL_MODEL) seam and the lifecycle-transition state machine the eval surface does not govern — the Go ExtractFacts exists and is wired into the eval surface, but the judge-driven semantics stay oracle-authoritative until the LLM seam contract is pinned"]
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 9 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle _list_beliefs_python state-filter mutation (active→archived) flips the battery (2 failed); restored green"
|
||||
},
|
||||
"blocks_full_switchover": ["extract_facts", "synthesize_observations", "run_lifecycle"]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "auto-context",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-25",
|
||||
"parity_basis": "src/go/auto_context truth.json replay (go test -race -short green) + tests/test_auto_context_go_parity.py (8 passed)",
|
||||
"go_engine": {
|
||||
"path": "src/go/auto_context",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — build_auto_context, load_active_tasks; memory_dir-addressed; the warm-router/memory-store subprocess seams shell the same Python scripts the oracle calls) | CLI + replay",
|
||||
"delegation": "src/rune/auto_context.py: build_auto_context and load_active_tasks resolve through the Go binary; escape hatch RUNE_AUTO_CONTEXT_ENGINE=python; binary override RUNE_AUTO_CONTEXT_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "src/rune/rune_tui.py (+ canary harnesses)",
|
||||
"switched_how": "auto-context build/task-load call sites resolve through the Go engine at runtime",
|
||||
"receipt": "battery 8 passed",
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 8 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle _load_active_tasks_python status-filter mutation (drops in_progress) flips the battery (2 failed); restored green"
|
||||
},
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "cache",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-24",
|
||||
"parity_basis": "src/go/cache go test -race -short green (incl. frozen_truth.json + truth_hash.json replays) + tests/test_cache_go_parity.py (13 passed, both engines against the SAME cache.db)",
|
||||
"go_engine": {
|
||||
"path": "src/go/cache",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — get_cache, put_cache, invalidate, invalidate_scope, invalidate_all, cache_stats, handle_hook over the truth-tested Env library; ops carry memory_dir so the facade addresses any cache.db) | CLI + truth replays",
|
||||
"delegation": "src/rune/cache.py: get_cache, put_cache, invalidate, invalidate_scope, invalidate_all, cache_stats, handle_hook resolve through the Go binary; the engine mutates the same cache.db (cross-engine coherence tests pin both directions); escape hatch RUNE_CACHE_ENGINE=python; binary override RUNE_CACHE_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"contract_note": "the miss contract is the oracle's {hit: false, key} dict — the Go eval's nil-miss is translated at the facade seam so both engines expose the identical shape",
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "src/rune/memory_continuity.py, memory_store.py, persona_definition.py, rune_tui.py, orchestration/retrieval_benchmark.py",
|
||||
"switched_how": "cache seam call sites (put_cache/get_cache/invalidate*) resolve through the Go engine at runtime",
|
||||
"receipt": "tests/test_cache_go_parity.py green (13)",
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 13 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle _hash digest-width mutation flips the battery (2 failed); restored green"
|
||||
},
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "message-queue",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-25",
|
||||
"parity_basis": "src/go/message_queue truth.json replay (go test -race -short green) + tests/test_message_queue_go_parity.py (14 passed, both engines against the SAME queue.json)",
|
||||
"go_engine": {
|
||||
"path": "src/go/message_queue",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — detect_task_addition, add_message, get_pending, approve, reject, approve_all, clear, get_approved, mark_executed, get_status over the truth-tested MessageQueue library; ops carry queue_dir; QueuedMessage projected snake_case via toDict) | CLI + replay",
|
||||
"delegation": "src/rune/message_queue.py: MessageQueue.detect_task_addition, add_message, approve, reject, approve_all, clear, mark_executed, get_status resolve through the Go binary; the facade reloads the in-memory mirror after engine mutations (cross-engine coherence pinned both directions); escape hatch RUNE_MESSAGE_QUEUE_ENGINE=python; binary override RUNE_MESSAGE_QUEUE_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "message_queue.py main() CLI + any chat-mode consumer",
|
||||
"switched_how": "all queue seam operations resolve through the Go engine at runtime",
|
||||
"receipt": "tests/test_message_queue_go_parity.py green (14)",
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 14 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle _detect_task_addition_python imperative-verb weight mutation (0.4→0.5) flips the battery (detect-parity case fails); restored green"
|
||||
},
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "paths",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-25",
|
||||
"parity_basis": "src/go/paths go test -race -short green (frozen truth replay) + tests/test_paths_go_parity.py (9 passed)",
|
||||
"go_engine": {
|
||||
"path": "src/go/paths",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — repo_root, crate_dir, deploy_root, rune_home, harness_scripts, wait_resume_dir; the library previously had NO CLI) | frozen-truth replay",
|
||||
"delegation": "src/rune/paths.py: all six resolvers resolve through the Go binary (env-override semantics identical per row: RUNE_DEPLOY_ROOT/RUNE_HOME/HARNESS_SCRIPTS/RUNE_WR_CODE); escape hatch RUNE_PATHS_ENGINE=python; binary override RUNE_PATHS_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"dichotomy_note": "the Go side carries the Law 7 verdict header (simple operations stay in the caller's language — path resolution is pure string/env work where FFI overhead would exceed the compute); the wave wires the DELEGATION anyway per the audit's zero-delegation finding, and the battery pins byte-identical resolution both directions",
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "src/rune/behavior.py, build_mode.py, canary harnesses, memory_env (20+ importers)",
|
||||
"switched_how": "every resolver call site resolves through the Go engine at runtime",
|
||||
"receipt": "battery 9 passed",
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 9 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle _deploy_root_python default-path mutation flips the battery (1 failed); restored green"
|
||||
},
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "pins",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "closed-verdict",
|
||||
"method": "CLASSIFY (owner-amended): decide from the consumers — port to Go or record the Python-boundary verdict",
|
||||
"decided_at": "2026-10-25",
|
||||
"verdict": "PYTHON-BOUNDARY with RUST-KERNEL AUTHORITY — no Go wiring",
|
||||
"audit_correction": "the audit's premises were both stale: (1) a Go pins port EXISTS (src/go/pins, batch 58) but carries only a recall kernel over (state, item_id) — no details-blob projection, no unpin, no store; (2) pins is NOT unwired — its recall/unpin/list/store-path kernels already resolve through the Rust cognition_core (src/rust/cognition_core broker.rs pins_recall/pins_unpin/pins_list/pins_store_path, live per the identity_anchors._cc_core probe verified this wave)",
|
||||
"reasoning": {
|
||||
"no_duplicate_authority": "wiring a Go delegation would introduce a THIRD engine over semantics the Rust kernel already governs and enforces (parity-proven against the frozen oracle in frozen_pins_recall.json, real pin shapes; the Python fallback was retired 2026-10-10 as delegation-only with RuntimeError on absent kernel — no silent second implementation)",
|
||||
"consumer_evidence": "the TUI-facing consumer (rune_tui.py tasks lane, pins.list_pins()) and the microkernel hook both resolve through the cc-kernel branch; the canonical store (PinRecord JSON under memory_dir/store/pins.json, AtomicStore temp+rename) is the reactive Python surface that justifies the boundary",
|
||||
"go_kernel_disposition": "surplus parity artifact — kept for the recall-semantics replay; NOT the delegation target"
|
||||
},
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "src/rune/rune_tui.py (tasks lane), microkernel hook dispatch",
|
||||
"switched_how": "no switch needed — already delegated to the Rust kernel",
|
||||
"receipt": "tests/test_pins_boundary_verdict.py (4 passed, topology pinned so drift flips) + tests/test_legacy_pins.py green (5)",
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"python": "verdict battery 4 passed; legacy pins 5 passed",
|
||||
"ruff": "check + format clean on touched files"
|
||||
},
|
||||
"blocks_full_switchover": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema": "integrated-run/1",
|
||||
"capability": "voicebox",
|
||||
"contract": "ADMIT-wave-rebase-completion",
|
||||
"issue": "Gitea #2587 (REBASE-COMPLETION)",
|
||||
"phase": "building",
|
||||
"method": "PORT → INTEGRATE → REMOVE (owner-amended): live consumers switched to the Go engine; Python oracle retained; NO Python deletion this wave",
|
||||
"integrated_at": "2026-10-25",
|
||||
"parity_basis": "src/go/voicebox truth.json replay (go test -race -short green, 2 documented improvements applied) + tests/test_voicebox_go_parity.py (6 passed)",
|
||||
"go_engine": {
|
||||
"path": "src/go/voicebox",
|
||||
"surface": "eval <ops.json> (new this wave: eval.go — handle_hook, manifest) | replay",
|
||||
"delegation": "src/rune/voicebox.py: handle_hook resolves through the Go binary; escape hatch RUNE_VOICEBOX_ENGINE=python; binary override RUNE_VOICEBOX_BIN",
|
||||
"fail_mode": "fail-loud RuntimeError when the Go engine is missing or errors (no silent Python fallback)"
|
||||
},
|
||||
"documented_divergence": "the engine's empty-event validation improvement surfaces through the delegation: an EMPTY event returns status 'error' where the oracle silently returned ok (the oracle body's lie is retained as oracle behavior only) — battery test_empty_event_engine_improvement pins both sides of the divergence",
|
||||
"consumers": [
|
||||
{
|
||||
"consumer": "microkernel hook dispatch (voicebox registers as a plugin)",
|
||||
"switched_how": "handle_hook call sites resolve through the Go engine at runtime",
|
||||
"receipt": "battery 6 passed",
|
||||
"blocks_full_switchover": ["speak/listen/talk stay Python: the audio loop is pynput/venv-bound (the Go port records the same boundary — porting it would break the UI law)"]
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"go": "go test -race -short ./... green; go vet clean; binary rebuilt from source",
|
||||
"python": "battery 6 passed",
|
||||
"ruff": "check + format clean on touched files",
|
||||
"mutation_flip": "oracle handler-name mutation (default→voicebox) flips the battery (3 failed); restored green"
|
||||
},
|
||||
"blocks_full_switchover": ["speak", "listen", "talk"]
|
||||
}
|
||||
Reference in new issue
Block a user