Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
206704c
agent_console: stack-native conversational agent (context-runtime + r…
Jul 6, 2026
8ac83ed
agent_console: on-prem model plane + resilient model calls
Jul 6, 2026
86b9b87
sidekick[mcp-module]: Implement MCP adapter module
Jul 6, 2026
9202dc7
sidekick[mcp-tests]: Hermetic MCP adapter tests
Jul 6, 2026
46080e3
mcp: bundled web_search server + AgentConsole.mount_mcp (proof: MCP t…
Jul 7, 2026
579aba0
supply_chain: software-supply-chain inspection plane (Trivy/Syft) — L…
Jul 7, 2026
8a371e9
sidekick[vuln-feeds-module]: Implement vuln_feeds ingestion module
Jul 7, 2026
98e2894
sidekick[vuln-feeds-tests]: Hermetic tests for vuln_feeds
Jul 7, 2026
dd8443e
supply_chain: add scan_rootfs (whole-container OS+deps) — surfaces ba…
Jul 7, 2026
5d16d5f
sidekick[scanner-methods]: Add container-scan + triage methods to Sup…
Jul 7, 2026
92d09a6
sidekick[scanner-tests]: Hermetic tests for container scan + triage
Jul 7, 2026
9b5a1f4
vuln_db: read-only Doris vuln-DB client with permissioning (the NVD/O…
Jul 7, 2026
525feb3
supply_chain: classify OS-base vs app-dependency findings + advise() …
Jul 7, 2026
b47a34d
optimizer: governance seam — optional PolicyProvider + TrustProvider
Jul 7, 2026
6868252
registry: capability registry — capabilities in the planner, not the …
Jul 7, 2026
d68cb4d
optimizer: Generation 4 online learning — BanditOptimizer + off-polic…
Jul 7, 2026
0a67aef
optimizer: Generation 5 trust-aware execution — trust objective + hon…
Jul 8, 2026
343bac3
learning: Phase 4 async learning loop — outcome events -> aggregator …
Jul 8, 2026
2939dec
learning: KafkaEventBus binding — the 'nervous system' transport for …
Jul 8, 2026
46f45b5
adapters: Phase 5 temporal / bi-temporal retrieval — 'what changed, a…
Jul 8, 2026
72f6c0a
bandit: discounted (recency-weighted) updates — the non-stationarity …
Jul 8, 2026
58e4db7
tools: data-access authorization seam on ToolRegistry (open-core)
Jul 8, 2026
991fb72
integrations: job_leads — AI-hiring lead-gen + tailored outreach (Mar…
Jul 8, 2026
f4f4581
job_leads: AdzunaSource — live jobs-API search (free tier, ToS-friend…
Jul 8, 2026
3e499fa
job_leads: per-user lead profiles + current_principal() so tools scop…
Jul 8, 2026
72a04f1
policy: Policy Runtime core — commands, rules (long-term memory), dec…
Jul 8, 2026
59195c3
policy: command factories (policy_commands/global_policy_commands) + …
Jul 8, 2026
4530c94
benchmark: add v3 (preliminary) column — online optimization under drift
Jul 8, 2026
ca28c08
benchmark: drop v3 preliminary block — keep the page to shipped v1/v2…
Jul 8, 2026
9297bf3
benchmark: commit the self-contained HTML site card (v1/v2, both runt…
Jul 8, 2026
97b353e
benchmark(v3): preliminary drift-adaptation doc + regenerable --v3-do…
Jul 8, 2026
292db10
agent_console: how-to/what-is questions route to help, not a keyword-…
Jul 8, 2026
57943f2
agent_console: explain-questions get domain-complete answers, not a b…
Jul 8, 2026
4f05b15
proxmox-demo: route contextruntime model plane at the shared Qwen shi…
Jul 8, 2026
43e561b
judge: default to Grok 4.5 + v3 benchmark report
Jul 9, 2026
e44ed9f
control-plane: multi-tenant corpus models for the chat demo (CR_TENANTS)
Jul 9, 2026
5b0ddcd
feat(planner): knowledge-aware routing — temporal as a first-class re…
arybach Jul 10, 2026
e2a07b5
bench: add guide + growth-assistant tenants to the fleet catalog
Jul 10, 2026
8e4ffeb
v4: wire knowledge-aware planning (classify → constrain → learn)
Jul 11, 2026
5a6485f
hipporag: default LLM gpt-4o-mini -> gpt-5-mini (gpt-4o-mini deprecat…
arybach Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ venv/
.sidekick/
.sidekick-*/
.financebench/
.medical/
6 changes: 6 additions & 0 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ abstention that v1 lacks, and a ~62% depth cut with precision rising to 100%._

## Retrieval over heterogeneous personal data (financial × medical)

> **Now live** as the **"Context Runtime · Mixed"** model on chat.redevops.io — the same
> coverage-routed sharding, served from `CR_TENANTS=…mixed=shards(finance:/corpus,medical:/medical)`.
> The live demo's medical shard is the public **PubMedQA** corpus (`deploy/medical/`, ~3.3k
> passages); the measurement below used the smaller **16-note curated collision set** from
> `examples/heterogeneous_shards.py` (built to maximize the vocabulary overlap the result turns on).

**`examples/heterogeneous_shards.py`** — the interesting, non-obvious one.

A real user's local files are a mix of very different data. This runs against **real
Expand Down
21 changes: 21 additions & 0 deletions benchmarks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html><meta charset="utf-8"><title>Context Runtime — v1 vs v2 benchmarks</title>
<style>
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#0f172a;background:#f8fafc;margin:0;padding:2rem}
.wrap{max-width:960px;margin:0 auto}
h1{font-size:1.5rem;margin:0 0 .25rem} .sub{color:#64748b;margin:0 0 1.5rem}
table{border-collapse:collapse;width:100%;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08)}
th,td{padding:.7rem .9rem;text-align:right;border-bottom:1px solid #eef2f7}
th{background:#0f172a;color:#fff;font-weight:600;font-size:.85rem}
td.metric{text-align:left} td.metric .d{display:block;color:#94a3b8;font-size:.78rem;font-weight:400;margin-top:.15rem}
td.num{font-variant-numeric:tabular-nums;color:#334155}
td.delta{font-variant-numeric:tabular-nums;font-weight:600;color:#059669}
tr:last-child td{border-bottom:0}
.foot{color:#94a3b8;font-size:.8rem;margin-top:1rem}
.grp{background:#f1f5f9;color:#475569;font-size:.75rem;text-align:center;letter-spacing:.04em}
</style>
<div class="wrap">
<h1>Context Runtime — v1 → v2, measured in both runtimes</h1>
<p class="sub">The same seeded, ground-truth retrieval simulation, run in the Python source-of-truth and the Go port. v2 = calibrated relevance-in-reward + abstention + the DSpark load-aware sizer.</p>
<table><thead><tr><th>Metric</th><th>Py v1</th><th>Py v2</th><th>Δ</th><th>Go v1</th><th>Go v2</th><th>Δ</th></tr></thead><tbody><tr><td class="metric"><b>Learned-policy precision</b><span class="d">The served passages that are actually relevant, after the policy converges. v2's reward finally sees calibrated relevance.</span></td><td class="num">67.6%</td><td class="num">82.2%</td><td class="delta">▲ +14.6 pts</td><td class="num">84.6%</td><td class="num">95.9%</td><td class="delta">▲ +11.3 pts</td></tr><tr><td class="metric"><b>Abstention recall (unanswerable caught)</b><span class="d">Share of truly-unanswerable queries v2 declines to answer. v1 has no abstention at all.</span></td><td class="num">0.0%</td><td class="num">100.0%</td><td class="delta">▲ +100.0 pts</td><td class="num">0.0%</td><td class="num">100.0%</td><td class="delta">▲ +100.0 pts</td></tr><tr><td class="metric"><b>False-abstain rate (answerable dropped)</b><span class="d">Answerable queries v2 wrongly declined — the cost of abstention. Lower is better.</span></td><td class="num">0.0%</td><td class="num">0.0%</td><td class="num">—</td><td class="num">0.0%</td><td class="num">0.0%</td><td class="num">—</td></tr><tr><td class="metric"><b>Expensive-stage depth (passages)</b><span class="d">Passages sent to the costly rerank/synthesis stage from a deep k=8 arm. The sizer prunes the low-relevance tail.</span></td><td class="num">8.00</td><td class="num">3.00</td><td class="delta">▼ −62%</td><td class="num">8.00</td><td class="num">3.00</td><td class="delta">▼ −63%</td></tr><tr><td class="metric"><b>Precision after the sizer</b><span class="d">Precision of what survives the sizer's gate — pruning the tail raises it.</span></td><td class="num">37.5%</td><td class="num">100.0%</td><td class="delta">▲ +62.5 pts</td><td class="num">37.5%</td><td class="num">100.0%</td><td class="delta">▲ +62.5 pts</td></tr></tbody></table>
<p class="foot">40-seed average · precision headlined at β=0.9 (the calibration-trust knob; shipped default 0.5) · Go is an independent re-implementation on identical methodology — directional parity across languages.</p>
</div>
68 changes: 68 additions & 0 deletions context_runtime/abstention.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Honest abstention — Generation 5 (Whitepaper v3, Trust-Aware Execution).

Production AI systems fail when operators stop trusting them, and the fastest way to burn trust is a
confident, wrong answer. So a trust-aware planner must be willing to **decline**: when the best available
plan's calibrated confidence is below the bar, abstain (or escalate to a stronger tier) rather than serve.
Abstaining beats guessing — an honest "I don't know" protects trust, and the TrustLedger credits it.

This gate is optimizer-agnostic: it reads a ``PlanScore`` (the estimator's plan-time confidence, optionally
run through a calibration map so the number means P(correct), not a raw score) and returns a verdict. The
online optimizer records the verdict in ``Plan.extra["abstention"]``; a consumer serves, escalates, or
declines accordingly.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Callable

from .types import Goal, PlanScore


@dataclass(frozen=True)
class AbstentionVerdict:
action: str # "serve" | "escalate" | "abstain"
confidence: float # calibrated confidence of the evaluated plan, in [0, 1]
reason: str

@property
def abstained(self) -> bool:
return self.action == "abstain"


class AbstentionGate:
"""Decide whether a plan is confident enough to serve.

``min_confidence`` — the bar; below it we do not serve.
``calibrate`` — optional map from the raw plan-time confidence to a calibrated P(correct)
(e.g. the DSpark ``CalibrationMap``); identity if omitted.
``can_escalate`` — optional predicate ``(score, goal) -> bool``: when confidence is below the bar
but a stronger option exists, prefer escalation over a flat abstention.
"""

def __init__(
self,
min_confidence: float = 0.5,
*,
calibrate: Callable[[float], float] | None = None,
can_escalate: Callable[[PlanScore, Goal | None], bool] | None = None,
):
self.min_confidence = min_confidence
self.calibrate = calibrate
self.can_escalate = can_escalate

def confidence(self, score: PlanScore) -> float:
"""Plan-time confidence = the estimator's expected accuracy, calibrated if a map is provided."""
raw = score.expected_accuracy
return self.calibrate(raw) if self.calibrate else raw

def evaluate(self, score: PlanScore, goal: Goal | None = None) -> AbstentionVerdict:
c = self.confidence(score)
if c >= self.min_confidence:
return AbstentionVerdict("serve", c, f"confidence {c:.2f} ≥ {self.min_confidence:.2f}")
if self.can_escalate is not None and self.can_escalate(score, goal):
return AbstentionVerdict(
"escalate", c, f"confidence {c:.2f} < {self.min_confidence:.2f} — escalate to a stronger tier"
)
return AbstentionVerdict(
"abstain", c, f"confidence {c:.2f} < {self.min_confidence:.2f} — abstain (honest, protects trust)"
)
109 changes: 109 additions & 0 deletions context_runtime/adapters/model_openai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""OpenAICompatibleModel — a dependency-light ``ModelPlugin`` (SPEC §4.3).

Speaks the OpenAI ``/chat/completions`` wire format over the *stdlib* (urllib), so it
drives OpenAI gpt-5.x, DeepSeek, vLLM, Ollama — any compatible endpoint — with the same
``ModelRequest → ModelResult`` contract and cost-tiered ``Tier`` routing as
``LiteLLMModel``, but **without the litellm dependency**. That matters because the slim
agent containers install only the base ``context-runtime`` (no ``[litellm]`` extra): this
adapter lets those apps run their conversational agent *on the Context Runtime model
plane* rather than reaching around it to a raw provider SDK. Degrade to ``StubModel``
when no key is configured (``from_env`` returns ``None``).
"""
from __future__ import annotations

import json
import os
import urllib.error
import urllib.request

from ..types import ModelCapabilities, ModelRequest, ModelResult, PluginInfo
from .model_litellm import Tier # dataclass only; importing it does not pull in litellm


class OpenAICompatibleModel:
"""ModelPlugin over an OpenAI-compatible endpoint using only the stdlib."""

def __init__(self, tiers: list[Tier], default_tier: str = "chat", timeout: float = 40.0):
self.tiers = {t.name: t for t in tiers}
self.default_tier = default_tier
self.timeout = timeout

@classmethod
def from_env(
cls,
*,
model_env: str = "AGENT_LLM_MODEL",
default_model: str = "grok-4.5",
key_envs: tuple[str, ...] = ("AGENT_LLM_KEY", "OPENAI_API_KEY"),
base_envs: tuple[str, ...] = ("AGENT_LLM_BASE_URL", "OPENAI_BASE_URL"),
cost_per_1k: float = 0.0,
) -> "OpenAICompatibleModel | None":
"""Build from environment, or ``None`` when nothing is configured (offline → StubModel).

Priority: an explicit OpenAI/agent key, else the self-hosted OpenAI-compatible endpoint
the agentic-os apps already point at (``REDEVOPS_LLM_BASE_URL`` / ``REDEVOPS_LLM_MODEL`` —
typically a keyless on-prem DeepSeek). That keeps the agent on our own model plane without
spreading a provider key across every container.
"""
key = next((os.environ[k] for k in key_envs if os.environ.get(k)), None)
if key:
base = next((os.environ[b] for b in base_envs if os.environ.get(b)), "https://api.openai.com/v1")
model = os.environ.get(model_env, default_model)
return cls([Tier(name="chat", model=model, base_url=base.rstrip("/"), api_key=key, cost_per_1k=cost_per_1k)])
rbase = os.environ.get("REDEVOPS_LLM_BASE_URL")
if rbase:
rmodel = os.environ.get("REDEVOPS_LLM_MODEL", "DeepSeek-V4-Flash")
rkey = os.environ.get("REDEVOPS_LLM_KEY") or "sk-noauth" # vLLM ignores it; header must exist
return cls([Tier(name="chat", model=rmodel, base_url=rbase.rstrip("/"), api_key=rkey, cost_per_1k=cost_per_1k)])
return None

def _tier_for(self, capability: str) -> Tier:
return self.tiers.get(self.default_tier) or next(iter(self.tiers.values()))

def complete(self, req: ModelRequest) -> ModelResult:
tier = self._tier_for(req.capability)
messages: list[dict] = []
if req.system:
messages.append({"role": "system", "content": req.system})
messages.extend(dict(m) for m in req.messages)
# gpt-5.x wants max_completion_tokens; self-hosted vLLM/DeepSeek want max_tokens. Branch on host.
tok_key = "max_completion_tokens" if "openai.com" in (tier.base_url or "") else "max_tokens"
payload: dict = {"model": tier.model, "messages": messages, tok_key: req.max_tokens}
if req.tools:
payload["tools"] = list(req.tools)
request = urllib.request.Request(
tier.base_url + "/chat/completions",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + (tier.api_key or ""), "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=self.timeout) as resp:
d = json.load(resp)
except urllib.error.HTTPError as e: # surface provider errors so the caller can fall back
detail = e.read()[:300].decode("utf-8", "ignore")
raise RuntimeError(f"model http {e.code}: {detail}") from e
choice = (d.get("choices") or [{}])[0]
text = (choice.get("message") or {}).get("content") or ""
usage = d.get("usage") or {}
ptoks = int(usage.get("prompt_tokens") or self.count_tokens(json.dumps(messages), tier.model))
ctoks = int(usage.get("completion_tokens") or self.count_tokens(text, tier.model))
cost = (ptoks + ctoks) / 1000.0 * tier.cost_per_1k
return ModelResult(
text=text.strip(),
model=tier.model,
tier=tier.name,
prompt_tokens=ptoks,
completion_tokens=ctoks,
est_cost_usd=round(cost, 6),
models_used=(tier.model,),
)

def capabilities(self, model: str) -> ModelCapabilities:
return ModelCapabilities(max_context_tokens=128000, tool_calling=True, structured_outputs=True)

def count_tokens(self, text: str, model: str) -> int:
return max(1, len(text) // 4) # ~4 chars/token, matches StubModel's estimate

def info(self) -> PluginInfo:
return PluginInfo(name="openai_compatible", kind="model", capabilities=frozenset(self.tiers))
2 changes: 1 addition & 1 deletion context_runtime/adapters/store_hipporag.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class HippoRAGRetriever:
"""The real graph retriever — wraps redevops-io/HippoRAG. Lazy-imports so the core
package and the offline path don't need its (heavy) deps."""

def __init__(self, save_dir: str = ".context_runtime/hipporag", llm_model_name: str = "gpt-4o-mini",
def __init__(self, save_dir: str = ".context_runtime/hipporag", llm_model_name: str = "gpt-5-mini",
embedding_model_name: str = "nvidia/NV-Embed-v2", source: str = "graph"):
self.save_dir = save_dir
self.llm_model_name = llm_model_name
Expand Down
32 changes: 23 additions & 9 deletions context_runtime/adapters/store_router.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,56 @@
"""HopRouterRetriever — dispatch single-hop vs multi-hop by retrieval method (SPEC §4.5).

The control plane's per-query decision made physical: ``method="graph"`` → the graph
retriever (HippoRAG / SimGraph); everything else → the single-hop retriever
(redevops-rag / in-memory). Itself a RetrieverPlugin, so the runtime holds ONE
retriever and the planner's method choice routes transparently underneath.
The control plane's per-query decision made physical, routing by knowledge representation:
``method="graph"`` → the graph retriever (HippoRAG / SimGraph); ``method="temporal"`` → the
bi-temporal store (Graphiti / in-memory TemporalStore); ``method="community"`` → the community
retriever; everything else → the single-hop retriever (redevops-rag / in-memory). Itself a
RetrieverPlugin, so the runtime holds ONE retriever and the planner's method choice routes
transparently underneath.

The graph/community/temporal slots are OPTIONAL: an unwired slot (or, for temporal, a store
with no matching fact) falls back to single-hop retrieval, so adding a representation never
regresses default behavior — it only *adds* a route the planner can take once a backend is bound.
"""
from __future__ import annotations

from ..types import Hit, PluginInfo, Retrieval

_GRAPH_METHODS = {"graph"}
_COMMUNITY_METHODS = {"community"}
_TEMPORAL_METHODS = {"temporal"}


class HopRouterRetriever:
def __init__(self, single_hop, graph, community=None):
self.single_hop = single_hop # RetrieverPlugin: bm25/vector/hybrid
def __init__(self, single_hop, graph, community=None, temporal=None):
self.single_hop = single_hop # RetrieverPlugin: bm25/vector/hybrid (document)
self.graph = graph # RetrieverPlugin: graph/multi-hop
self.community = community # RetrieverPlugin: community/global (optional)
self.temporal = temporal # RetrieverPlugin: bi-temporal facts (optional)

def search(self, query: str, k: int, method: Retrieval = "hybrid") -> list[Hit]:
if method in _COMMUNITY_METHODS and self.community is not None:
return self.community.search(query, k, method)
if method in _GRAPH_METHODS:
return self.graph.search(query, k, method)
if method in _TEMPORAL_METHODS and self.temporal is not None:
hits = self.temporal.search(query, k, method)
if hits: # a populated bi-temporal store answered
return hits
# unpopulated store / no temporal fact matched → fall back to single-hop
return self.single_hop.search(query, k, method)

def index(self, path: str) -> dict:
a = self.single_hop.index(path) if hasattr(self.single_hop, "index") else {}
b = self.graph.index(path) if hasattr(self.graph, "index") else {}
out = {"single_hop": a, "graph": b}
if self.community is not None and hasattr(self.community, "index"):
out["community"] = self.community.index(path)
for name, r in (("community", self.community), ("temporal", self.temporal)):
if r is not None and hasattr(r, "index"):
out[name] = r.index(path)
return out

def info(self) -> PluginInfo:
caps = set()
for r in (self.single_hop, self.graph, self.community):
for r in (self.single_hop, self.graph, self.community, self.temporal):
if r is not None and hasattr(r, "info"):
caps |= set(r.info().capabilities)
return PluginInfo(name="hop_router", kind="retriever", capabilities=frozenset(caps))
Loading