Skip to content

v4 → main: knowledge-aware planning (v4) + online-learning / trust / policy (v3)#4

Closed
arybach wants to merge 40 commits into
mainfrom
v4
Closed

v4 → main: knowledge-aware planning (v4) + online-learning / trust / policy (v3)#4
arybach wants to merge 40 commits into
mainfrom
v4

Conversation

@arybach

@arybach arybach commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Point main at the v4 generation branch.

Brings the v2 development line (Generations 4–6) onto main as one squash:

  • v4 — knowledge-aware planning: representation-first planner (classify → constrain → learn), temporal as a first-class representation, capability registry.
  • v3 — online learning (bandit + off-policy), trust-aware execution, policy-constrained planning, the policy runtime.

Preserves the main-only hipporag gpt-5-mini default (cherry-picked onto v4 so this squash doesn't revert it). Generation branches v3/v4 and tags v3.0/v4.0 are pushed. The Graphiti/HippoRAG engine bindings + structured-routing land as a follow-up PR (post-benchmark).

Recommend Squash and merge to match the existing main PR history.

redevops and others added 30 commits July 6, 2026 17:11
…edevops-rag + agent-harness)

Reusable AgentConsole every agentic-os app mounts as its chat surface. Runs
entirely on our own planes, no raw provider SDK:
  - LLM  = OpenAICompatibleModel (new stdlib ModelPlugin, cost-tiered Tier
           routing, ModelRequest->ModelResult) so slim images without the
           [litellm] extra still route through the Context Runtime model plane;
           degrades to StubModel offline.
  - grounding = PrimerIndex (redevops-rag hybrid_search contract; [rag] extra
           swaps in the reranked path) for how-do-I / explain answers.
  - actions = agent-harness ToolRegistry + ApprovalPolicy — side-effecting
           tools are gated and every decision audited.
respond() returns a transparent show-your-work payload; panel_html() the panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenAICompatibleModel.from_env now falls back to the self-hosted OpenAI-compatible
endpoint the apps already point at (REDEVOPS_LLM_BASE_URL / REDEVOPS_LLM_MODEL,
keyless DeepSeek) when no provider key is set — the agent runs on our own model
plane without a key in every container. Branch the token param (max_completion_tokens
for OpenAI, max_tokens for vLLM/DeepSeek). AgentConsole degrades to the grounded
passage / raw tool summary if the model call fails, so a model outage never 500s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ools in apps)

- context_runtime/tools/mcp_servers/web_search.py: a keyless, dependency-free MCP
  stdio server exposing a read-only web_search tool (Wikipedia + Hacker News/Algolia).
- AgentConsole.mount_mcp(client, prefix=, allow=): mounts an MCP server's tools into
  the agent-harness registry AND the classify/dispatch catalog, so the assistant can
  pick + run them like native tools. readOnlyHint tools run ungated; side-effecting
  ones stay gated. Proven end-to-end: console routes 'search the web…' to web_search,
  runs it over real stdio, returns live results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ightwell pre-deploy half

SupplyChainScanner wraps Trivy (CVEs in OS+language deps, IaC misconfig, secrets, SBOM)
and Syft (SBOM) as subprocesses, normalizes findings (id, pkg, installed, fixed pin,
severity), and degrades gracefully when the binaries aren't installed. The pre-deploy
half of the Security & Compliance block; Edge Sentinel's AgentConsole triages the output.
Hermetic tests parse canned Trivy JSON + cover the degrade path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se-image CVEs a lockfile scan misses

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…SV consumer)

VulnDB reads the vulns table vuln_feeds populates — lookup(package/cve/min_cvss) + enrich(findings)
for apps like Edge Sentinel to annotate scan results against our NVD/OSV store. Applies the same
row-scope + column-mask permissioning as the enterprise DorisStore (Principal: roles + owns_rows_of;
non-privileged callers see only owned sources, refs masked). pymysql deferred/optional; degrades to
available()=False. 5 hermetic tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…recommendation

Finding.cls (os|lang) from Trivy's Result.Class; advise() turns the split into an actionable
recommendation — when OS/base-image CVEs dominate (the common case), it recommends HARDENING THE
BASE (rebase to a patched python:3.12-slim digest / apt-get upgrade the flagged system packages),
fleet-wide, rather than chasing individual CVEs; otherwise it points at the app's own pins. Powers
Edge Sentinel's scan recommendation.
Two optional Protocols (plugins/base.py) let the enterprise open-core layer compose INTO the
cost optimizer without the engine importing any enterprise types:
 - PolicyProvider.feasible(candidate, goal, score) narrows the feasible space BEFORE cost
   ranking; a policy-rejected candidate is infeasible regardless of score, its reason recorded
   in Plan.rejected (observable in EXPLAIN).
 - TrustProvider.score(candidate, goal) breaks ties between equally cost-ranked feasible plans.

KnapsackOptimizer gains optional policy/trust params; both default None → identical pre-seam
behavior (verified: full suite green). Realizes the Whitepaper v3 pipeline seam
Goal -> Policy Engine -> Feasible Plans -> Cost Model -> Planner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…prompt

Whitepaper v3's scaling property: an assistant's abilities (retrieval methods, model tiers, …)
are registered OUT OF BAND with cost/quality priors + the intent buckets they serve; the planner
selects among them by intent/policy/cost, so capability count scales in a cheap registry rather
than in the model's context window.

 - registry/capabilities.py: Capability + CapabilityRegistry (register/get/list/for_intent).
   CapabilityRegistry.from_rules() lifts the existing planner rule tables into registry form, so
   the default registry is behavior-equivalent (single source of truth).
 - planner/candidates_registry.py: RegistryCandidateGenerator — a drop-in CandidateGenerator that
   draws methods/tiers from the registry instead of hard-coded tables (injectable via
   ContextRuntime(candidates=...)).
 - tests: bucket-by-bucket parity with RuleCandidateGenerator + registering a new capability
   widens the candidate set with no rules/prompt edit. example: examples/capability_registry.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y evaluation

Plan selection becomes a contextual bandit keyed by intent bucket (Whitepaper v3, Gen 4):
 - BanditOptimizer (optimizer/online.py) implements CostOptimizer. score() is unchanged
   (estimate + hard/policy feasibility). select() runs a subset-aware epsilon-greedy over the
   FEASIBLE candidates, seeded with the cost-model total as an optimistic prior and refined by
   measured reward; records selection propensity + mode in Plan.extra for off-policy eval.
   learn()/learn_from_plan() fold reward into the shared EpsilonGreedyBandit.
 - Layered exploration: Tier 3 = live epsilon branch; Tier 2 = shadow=True (off-path pick);
   Tier 1 = offpolicy_values() — self-normalized IPS over logged probabilities ranks arms the
   planner never served, at zero live latency.
 - Composes with the Phase-0 seam: a PolicyProvider filters the feasible space BEFORE any
   exploration (never explore an infeasible plan); a TrustProvider breaks ties.

Wiring: CostOptimizer.select gains an optional context="" (intent bucket); KnapsackOptimizer
ignores it, the runtime passes intent.bucket. Backward-compatible — full suite green.
6 tests (exploit/explore/learn/policy-filter/off-policy) + examples/online_learning.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…est abstention

Two Gen-5 behaviors (Whitepaper v3) on the online planner:
 - trust as an OBJECTIVE: BanditOptimizer gains trust_weight; when > 0 the trust score is folded
   into the ranking value (base = (1-w)*base + w*trust), not merely a tie-breaker, so a
   relied-upon plan can win over a nominally cheaper one.
 - honest abstention: abstention.py = AbstentionGate (serve / escalate / abstain) on a plan's
   calibrated confidence (expected_accuracy through an optional calibration map). Injected via
   abstain_gate; the verdict is recorded in Plan.extra['abstention']. Abstaining beats a
   confident-wrong answer — it protects operator trust (and the TrustLedger credits it).

Also fixes a latent bug: _arm_value referenced goal without receiving it (masked because Phase-2
tests set no TrustProvider on the bandit); it now takes goal, so trust works on the online planner.

8 tests + example; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-> snapshots

The policy loop that makes the planner scale (Whitepaper v3): the planner never learns on the hot
path. Executions emit OutcomeEvents to a bus; one aggregator folds them into the shared bandit off
the serving path and republishes a versioned snapshot; stateless replicas select against a local
snapshot and reconcile. Neither stream touches the model's context window.

 - learning/events.py: OutcomeEvent (context, arm, reward, propensity, abstention + operator signals);
   from_plan() reads the optimizer's Plan.extra. Serializable.
 - learning/bus.py: EventBus Protocol + thread-safe InMemoryBus (publish/poll). A Kafka binding drops
   into the same seam for a fleet.
 - learning/aggregator.py: LearningAggregator (one writer) drains events, updates the bandit (skips the
   arm on an abstention but still forwards trust signal via on_trust sink), tracks a version, and emits
   a LearnedStateSnapshot; apply_to() reconciles a replica. Idempotent by seq.

5 tests (event-from-plan, off-hot-path fold, snapshot distribution to a replica optimizer, idempotent
replay, trust sink) + examples/learning_loop.py. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the loop

Satisfies the same publish/poll EventBus seam as InMemoryBus, backed by Kafka, so the async policy
loop and the context-freshness/CDC stream run unchanged across stateless replicas. kafka-python is
imported lazily (engine stays dependency-free); JSON codec by default, pluggable codec_out/codec_in
for a typed event (OutcomeEvent) — plain dicts round-trip for the vuln/CDC stream. 3 tests via an
injected fake client (no broker) incl. the aggregator draining from the Kafka seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nd when?'

The forthcoming retrieval method (Whitepaper v3): a dependency-free bi-temporal store where every fact
carries valid time (valid_from/valid_to — when it's true in the world) and transaction time
(recorded_at — when we learned it), so the planner can answer point-in-time questions the other methods
can't. TemporalStore satisfies the RetrieverPlugin seam (method='temporal', added to the Retrieval
literal): search() = current state; as_of(at, known_at) = world-time + transaction-time travel (a late
correction doesn't silently rewrite what was known then); changes(since, until) = what began/ended
validity. Graphiti can back it as an optional binding. 5 tests + example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…property + a benchmark

Completes the Gen-4 non-stationarity property the whitepaper names ('discounted updates that fade
stale evidence'): EpsilonGreedyBandit gains an optional discount (constant step size); 0 (default) =
sample-average 1/n, unchanged. BanditOptimizer passes it through.

examples/online_vs_static_bench.py benchmarks a planner under drift (best plan A->C at t=200):
  static 0.20  ·  online plain 0.33  ·  online discounted 0.67   (post-drift, oracle 0.80)
i.e. a static v2 planner is pinned to the stale plan; plain sample-average online adapts poorly (200
stale samples drown the signal); discounting tracks the drift. 3 unit tests lock the estimator in.

Also surfaced (and the benchmark now avoids) a footgun: learn() must key on the plan's arm
(method:tier via learn_from_plan), not the bare method string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every tool call for every AgentConsole app funnels through ToolRegistry.run(). It now takes an optional
authorizer (principal, ToolSpec, args)->reason|None run BEFORE the side-effect gate; deny short-circuits
with an audited [blocked] result. set_default_authorizer(fn) installs one process-wide, so a single
enterprise install() call gates the whole console fleet. AgentConsole.__init__ accepts authorizer=,
respond(message, principal=) threads the caller identity through. Default None ⇒ unchanged (verified:
full suite green). This is the ToolRegistry analog of the PolicyProvider seam — engine imports no
enterprise code. 3 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ket Radar)

A company hiring an AI Data Engineer/Developer in-house signals real internal AI-adoption pain. This
module sources such listings, drops consultancies/forward-deployed/agency shops (they build AI FOR
clients — no internal pain), writes a listing-tailored pitch from an EDITABLE, shared template
(placeholders {title}{company}{match}; {match} is LLM-tailored to the listing when a model is given),
dedupes via an append-only OutreachLedger so a listing that reappears months later is never pitched
twice, and drafts distribution to an outbox (real sending is an approval-gated deployment step).
Pluggable sourcing (StaticJobSource/CallableJobSource over web_search/a job API); resume attached when
the listing asks for a CV. Dependency-light + fully testable offline. 7 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e to the caller

 - tools/base.py: current_principal() — the in-flight tool call's principal (set by ToolRegistry.run
   around the call, reset after), so a tool can scope to the user without changing the ToolPlugin API.
 - job_leads: LeadProfile + ProfileStore (per-user, persisted <dir>/<user>.json). The owner
   (LEAD_OWNER_USER) is seeded with AI-infrastructure targets + the Context Runtime template; everyone
   else starts blank and configures their own include/exclude + template. classify_for()/find_leads_for()
   score against ONE user's targets; consultancies excluded by default. Templates are per-user now.
   6 tests (owner-vs-generic, isolation, profile-scoped classify/find).

Backward-compatible (default None principal ⇒ owner fallback); full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…isions+audit, AgentConsole wiring

Implements docs/policy-runtime.md phases 1-5 (OSS, open-core):
 - policy/store.py: Rule + RuleStore — persisted, scoped long-term memory (global | <app> | <app>:<user>),
   JSONL, atomic; content-hash ids; scopes_for(principal, app).
 - policy/plane.py: Decision (allow|deny|redact|flag|require_approval), PolicyDecision + DecisionSink (the
   mandatory audit event), GuardrailProvider (input/output, pattern) + ApprovalProvider (tool → require_approval),
   composed Policy.check() that emits a PolicyDecision for EVERY decision; set_default_policy/current_policy.
 - policy/commands.py: Command + CommandRegistry (dual-path /dispatch, permission-gated via injected can) +
   parse_args (flags + comma lists).
 - AgentConsole: /command dispatch (no LLM); input+output guardrails; tool-phase approval pauses irreversible
   actions; returns compact policy[] (progressive disclosure §10.1). All optional + fall back to the process
   default → one install governs the fleet; no policy ⇒ byte-for-byte unchanged (full suite green).

12 tests (RuleStore CRUD/scopes, guardrail deny/redact/phase, approval, per-user scope, parse_args,
command permission/help/alias, console dispatch/block/approval/fold/backcompat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…role_gate

Ready-made command sets over a RuleStore: /showpolicy /addpolicy /removepolicy /modifypolicy (aliases
/showguardrails /addguardrail /removeguardrail), read open + write gated by 'policy-admin'. role_gate()
is a simple role-based can(principal, requires) for deployments without the enterprise plane
(''=any · self=authenticated · policy-admin=admin role · role:X). 1 test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the consolidated v1-vs-v2 benchmark with a v3 section (Generation 4): the best plan drifts mid-run;
a static v1/v2 planner is pinned to the stale plan (post-drift reward 0.20) while the v3 online planner
re-explores and recency-weighted (discounted) learning tracks the shift (0.67; without discounting 0.34;
oracle 0.80). 24-seed average. A distinct axis from — and preserving — the v2 calibration gains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…imes)

Generated by examples/consolidated_benchmark.py --html. Final post-fix numbers
(bandit persist-context fix 1c2e135 already in v2). Hand-off artifact for the
redevops.io/benchmarks page; BENCHMARKS.md is the canonical markdown twin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
redevops and others added 10 commits July 8, 2026 11:48
…c writer

Re-adds v3_results() (online optimization under drift, Gen-4) to consolidated_benchmark.py behind a
--v3-doc PATH flag, and generates docs/BENCHMARKS-v3-preliminary.md. Kept off v2's shipped BENCHMARKS.md
and the default v1/v2 table — this is the v3 line's forward-looking axis (post-drift served-plan reward
0.20 static → 0.67 online, 24-seed avg, oracle 0.80).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…matched tool

The deterministic fallback (_keyword_route, used when the LLM classifier times out) scored a tool on a
single incidental word — 'How do I track just part of a page?' matched add_watch's 'monitor a page',
and 'What is dunning?' fired a real CHASE_OVERDUE. Interrogative/how-to leads now short-circuit to help,
mirroring the LLM classify rule; explicit action queries ('what changed this week?') still keyword-match.
Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…are one-liner

The help prompt said 'be concise — give the concrete steps', which is a how-to framing; for 'what is X?'
questions the model collapsed the answer to a single definition clause, dropping the surrounding domain
context that was in the retrieved passage (e.g. that http-bruteforce is a *scenario* that drives an
*alert*/*ban* on the source *IP*). Reworked the prompt to, for explain questions, define the term AND
explain how it fits the system from the passages. Validated A/B against the live model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m (Kimi fallback)

Point CR_UPSTREAM at the host model shim (host.docker.internal:8000 → shared Qwen3.6-35B-A3B on GPU0,
Kimi fallback in the shim) instead of Kimi directly; add host-gateway extra_hosts. Rebuilt on v3
context-runtime so redevops.io/planner (→ chat.redevops.io/librechat/explain) runs the v3 engine.
CR_MODEL_* overrides let you A/B another model without editing the compose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-existing working-tree changes, committed as part of the deploy sync:
- OpenAICompatibleModel default_model gpt-5.5 -> grok-4.5 (matches the dedicated
  CR_JUDGE_MODEL=grok-4.5 the live self-learning RAG now uses).
- examples/fleet_tenants.py: add an outreach tenant probe.
- docs/BENCHMARK-REPORT.md: the 30/30 v3 benchmark run + judge-swap note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore the three-model chat.redevops.io demo (wiped to FinanceBench-only by
the v3 deploy): one control plane serves finance/medical/mixed tenants, each
its own retriever + learned policy, routed by model id in /v1 and /librechat/*.

- CR_TENANTS "id=/corpus | id=shards(a:/p1,b:/p2)"; Mixed = coverage-routed
  finance x medical (cross-domain noise -> 0, the whitepaper win).
- Per-tenant cross-language (CR_TENANT_LANGS, opt-in) for e.g. Spanish speakers.
- deploy/medical: PubMedQA MedicalBench downloader + corpus builder.
- proxmox-demo: 3-model librechat.yaml + compose (judge + medical mount) + docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…presentation (#3)

Whitepaper v4 groundwork: the planner routes across knowledge *representations*, not
just retrieval algorithms. This adds temporal (bi-temporal) as a first-class routable
representation and a representation taxonomy, without a big refactor and fully backward
compatible.

- types: add `temporal` IntentBucket + a `KnowledgeRepresentation` taxonomy
- planner/representations.py (new): retrieval method -> representation map
  (document / graph / temporal / analytical / community / code / multimodal)
- planner/rules: temporal intent cues (as-of / what-changed / superseded / point-in-time),
  placed before multi_hop; BUCKET_DEFAULTS + BUCKET_TIERS (temporal candidate + hybrid fallback)
- store_router: optional `temporal` slot with empty-fallback — routes to the bi-temporal
  store only when one is bound and matches; otherwise falls back to single-hop, so a new
  representation never regresses default behavior
- costmodel: temporal recall + a temporal-bucket edge so the planner prefers the bi-temporal
  store for time-dependent questions (mirrors how multi_hop prefers graph)
- tests: 8 tests (routing, cost edge, router dispatch + fallback, bi-temporal point-in-time
  and what-changed semantics, representation taxonomy, end-to-end run)

The reference temporal backend is the in-memory TemporalStore already in the tree; a real
Graphiti engine binds via the same RetrieverPlugin seam at construction time. HippoRAG (graph)
already routes end-to-end; this closes the temporal gap. Full suite green.

Co-authored-by: redevops <redevops@redevops.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fleet_tenants.py claims to cover "every agentic-os-stack module", but guide and
growth-assistant (both live in modules.yaml / on demo.redevops.io) were missing —
so redevops.io had no measured learned-vs-baseline number for their cards.

Add both to CATALOG + PROBES so their numbers are reproducible from the same
harness as the rest of the fleet:
  guide             0.946 vs 0.800 (docs)
  growth_assistant  0.928 vs 0.800 (playbooks)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whitepaper-v4's representation-first planning was taxonomy-only (representations.py mapped
method→representation but nothing called it; candidates routed off the flat BUCKET_DEFAULTS).
Make it real, in three stages that compose with — not replace — the v2/v3 loop below them:

  1. Classify — RuleIntentAnalyzer now emits a KnowledgeRepresentation (Intent.representation).
     representations.classify() maps bucket→representation (multi_hop→graph, temporal→temporal,
     code→code) with content-hint overrides that reach representations no bucket produces on its
     own: "how many … per week" → analytical (OLAP), "screenshot" → multimodal.
  2. Constrain — RuleCandidateGenerator restricts methods to methods_for(representation) instead
     of the flat bucket table, keeping a document (hybrid) fallback so a missing/infeasible
     representation engine degrades gracefully, and widening under low confidence so uncertain
     intents explore across representations.
  3. Record & learn — representation is recorded on the plan, the librechat EXPLAIN output and the
     OutcomeEvent. A routed plan keeps its document fallback, so the bandit's arms span two
     representations — choosing between them IS representation selection, learned per context.

tests/test_representation_planning.py (7): classify head incl. analytical/multimodal hints,
analyzer sets representation, candidates constrained + document fallback, OLAP candidates now
reachable, bandit arms span representations, representation on the outcome event. Full suite
316 passed, 4 skipped (no regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion) (#1)

The HippoRAG indexing LLM defaulted to gpt-4o-mini, which OpenAI is deprecating.
Move to gpt-5-mini (the GPT-5 small tier). Still overridable via llm_model_name.

Co-authored-by: redevops <redevops@redevops.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@arybach

arybach commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded — main's squashed AgentConsole history conflicts with v4's granular version, so a direct v4→main squash can't auto-create. Replaced by a reconciled single-commit PR based on main.

@arybach arybach closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant