Skip to content

The RSI platform cycle — five layers, one hand-off

The loop kernel, compute allocator, promotion decision, capability foundry and continual experience are separately tested. RSIPlatform is the narrow system seam that makes them one cycle:

compute -> loop -> promote -> admit capability -> learn experience
from agentdescent import PlatformConfig, RSIPlatform
from agentdescent.durable_runs import SqliteRunStore

platform = RSIPlatform(
    SqliteRunStore("runs.sqlite3"),
    PlatformConfig("tenant-a", "ws-1", max_generations=3, target=0.9),
    compute=controller,          # ComputeController
    foundry=foundry,             # CapabilityFoundry (optional)
    experience=engine,           # ExperienceEngine (optional)
    decision_log=log, telemetry=telemetry)

run_id = platform.submit()
report = platform.run_cycle(
    run_id, "worker-1",
    step=my_step,                 # (LoopContext, ComputeDecision) -> LoopObservation
    evidence=my_evidence,         # (LoopReport, best) -> PromotionRequest
    capability=my_capability,     # (LoopReport, PromotionDecision) -> CapabilityRequest
)
report.complete, report.to_dict()

What the platform owns

  • Compute before the step. It calls ComputeController.choose() with the cycle's ComputeContext, passes the decision to step, observes the reward/cost, and writes compute_action / compute_option / compute_model / features into the observation evidence. A caller's own action key is preserved. Compute observation is at-least-once: a retried generation calls observe again, so callers that retry a failed step should account for that.
  • The durable loop. LoopKernel runs the generations; apply and rollback are passed straight through.
  • Promotion. The best accepted generation is handed to the evidence callback; promote() makes the decision with the platform's rules.
  • Foundry. The capability callback receives the loop report and promotion decision; a returned CapabilityRequest is admitted by the foundry.
  • Experience. Every generation becomes an ExperienceRecord (reward map from evidence["rewards"], action from evidence["compute_action"]), is ingested once, and the engine updates with the supplied validation/anchor rows. If no validation rows are supplied, the cycle's own records are used and the report marks self_validated=True — that is a warm-up, not held-out evidence. Anchors default to the most recent pre-cycle record; on the first cycle they bootstrap from the cycle itself. A campaign reuses the same rows every cycle unless the caller supplies fresh ones.
  • Completeness. A configured layer that does not produce a decision makes complete false. A layer skipped by an upstream decision (for example the foundry after a rejected promotion) is named in skipped and detail, never silently skipped.
  • Gate / risk gate. The constructor's optional gate / risk_gate are passed straight to LoopKernel; they act on the loop's control decision, not on promotion.

What it deliberately does not do

It adds no decision logic. It does not invent evidence, rerun a gate, or decide whether a promotion is good — it only hands the tested outputs to the next layer. A cycle with no evidence, capability or experience runs just the loop and is still complete.

From the CLI and the MCP server

The same campaign is available as a product surface:

agentdescent platform init --out platform.json --demo
agentdescent platform plan --spec platform.json
agentdescent platform run  --spec platform.json --json --out report.json

A platform spec names the SQLite run database, the two configs, and the callbacks by module:attribute:

{
  "runs_db": "platform-runs.sqlite3",
  "platform": {"tenant": "t1", "workspace": "ws", "max_generations": 3},
  "campaign": {"max_cycles": 5, "target": 0.9, "patience": 2},
  "compute": "mypkg.wiring:compute",
  "step": "mypkg.steps:step",
  "evidence": "mypkg.steps:evidence",
  "capability": "mypkg.steps:capability",
  "foundry": "mypkg.wiring:foundry",
  "experience": "mypkg.wiring:experience"
}

compute may be omitted (a single direct strategy over a zero-cost local model) or written inline:

{"models": [{"name": "local", "cost": 0.0}],
 "options": [{"name": "direct", "action": "direct", "calls": 1}]}

Budgeted PRM/verifier search as the step

step may be a {"budgeted": ...} block instead of a callable ref. It builds the existing BudgetedStep from budgeted_loop:

"step": {
  "budgeted": {
    "fabric": "mypkg.search:build_fabric",
    "state": "mypkg.search:state",
    "config": {"budgets": [8, 16, 32], "min_gain_per_expansion": 0.0001,
               "patience": 2},
    "risk": "mypkg.search:risk"
  }
}
  • fabric is a zero-argument builder (or a ready SearchFabric).
  • state is (LoopContext) -> state;
  • config is BudgetedStepConfig;
  • risk and evidence_of are optional callables.

The platform passes its own (context, compute_decision) to the step; the budgeted adapter ignores the compute decision because the search owns its budget. plan validates the refs and config without calling the builders, so a builder that raises is reported at run, not plan. scale_budget defaults to true, so the actual budgets are multiplied by the loop controller's budget_scale (the configured numbers are the base budgets).

plan resolves every ref and validates the configs without running. run returns the full PlatformCampaignReport with schema_version: 1. The MCP platform tool takes the same object and dry_run=true for the plan step. The spec executes the Python callbacks it names. For MCP, the operator must set AGENTDESCENT_SURFACE_ALLOW to the allowed module prefixes (an empty value refuses every ref), and AGENTDESCENT_SURFACE_WRITE_DIRS to the directories the server may create reports/DBs in. plan resolves refs without calling builders or creating the SQLite database.

Inspect a campaign after it finished

platform run keeps every cycle in its SQLite run database, so a process that has exited can still be inspected without SQL:

agentdescent platform status --runs-db platform-runs.sqlite3 \
  [--tenant t1] [--workspace ws] [--limit 20] [--json]
agentdescent platform status --runs-db platform-runs.sqlite3 --run-id RUN_ID

Without --run-id, the command lists the most recent runs, newest first, with status, best reward, generation count, accepted count and stop reason. With --run-id it returns that run's full LoopReport. It only reads the database and does not need the compute, foundry or experience callbacks. A missing database or unknown run is an input error (exit 2). The MCP platform_status tool exposes the same read-only view.