Skip to content

Decouple plugins from Go pipeline internals: session facts and serializable records #1499

Open

Description

@huang195

Part of #1437 (assessment: laptop data plane on Praxis) · prep work, useful to Cortex on its own

Status: there is no shipped Cortex–Praxis integration today (the paused cmd/cortex-praxis converter is not one), and the interim path is not decided (LLM traffic to praxis-ai as an inference server, or all traffic through Praxis via #1491). Nothing here depends on that choice: each item is useful to Cortex now, and would make a possible move to Praxis easier whichever path is taken.

Problem

Plugins are coupled to Go pipeline internals in two ways that cost Cortex today, and would also make it harder to run any plugin outside Cortex's Go pipeline (whether a Cortex plugin is ever ported is a separate question; #1437's work breakdown keeps the Go pipeline linked in-process):

  1. Plugins share data through Go structs. The parsed request is read by most plugins as a Go type, described in prose (docs/session-api.md, "Event schema") but with no machine-checked schema or fixtures. Praxis passes data between filters as filter_metadata strings or structured_metadata JSON.
  2. Three plugins read session history through a view that copies every event on each request. Praxis keeps no history, so those three couldn't run outside Cortex as written.

(Saved parser test cases were item 3 here; they moved to #1503.)

What Cortex does today

The parsed request is a Go struct.

Only three plugins read session history:

context-guru, session-budget, script and cpex read only Session.ID from the view. cpex also reads pctx.SessionSource, SessionHeader and ChainSession (cpex/cmf_body.go:74-89). tool-prune reads nothing from the session (it reads history from the request body).

Building the view copies every event. Store.View copies the session's event slice for each request whose session resolves, which on a laptop is nearly every request: in the forward proxy (server.go:1471) and in ext_proc (:584, :632). With session.max_events unset (the default), that grows without limit.

  • Measured on 2026-10-25 with a micro-benchmark of Store.View: about 33 KB and 2–4 µs per request at 100 events, 330 KB and 33–43 µs at 1,000, 1.6 MB and 0.18–0.19 ms at 5,000.
  • It is a shallow copy: about 320 bytes per event header, with the parsed request, invocations and message bodies shared, not duplicated. The allocation is real but short-lived.
  • The largest live laptop session held 3,481 events on 2026-10-25 (5,095 later the same day).
  • Small next to an LLM call. The stronger reason for session facts is that these three plugins can't run outside Cortex's Go pipeline while they need the full history.

Plugin-to-plugin records.

  • tool-prune reads the cost record as a Go value out of Extensions.Custom (toolprune/plugin.go:549).
  • litellm-budget-track reads the settled cost through settle.Load and rewrites it through settle.Amend, also Go values (settle.go:785-818).
  • The other direction already decodes: settle.Avoided reads tool-prune's event through a JSON round trip.

Praxis has candidate equivalents for three plugins (parity not verified):

  • opa (746 non-blank lines): PPE ships an OPA engine (praxis-proxy/policy, crates/builtins/src/pdps/opa).
  • jwt-validation (747, including validation/): PPE identity_jwt. Mainly used by the cluster sidecars, not the laptop.
  • static-inject (283): Praxis's headers and credential_injection filters.

Proposal

  1. Saved test cases for the parsers — moved to Turn the MCP, A2A and inference parsers into libraries, with saved real-traffic test cases #1503, which turns the parsers into libraries and uses the test cases as its safety net.
  2. Write down the parsed-request contract. A JSON schema for InferenceExtension, MCPExtension and A2AExtension, generated or checked from the Go types, linked from docs/session-api.md. Coordinate with Feed recording, sessions and cost through one input that does not depend on the Go listeners #1497 item 5 (event schema) and Make agentop and the installer depend only on documented contracts #1500 item 2 (stats fixtures): one schema-and-fixtures effort.
    • Move MCP Params/Result to RawJSON (also RPCID any and MCPError.Data any). Consumers: cpex, ibac, lineage, opa, sparc and the session API's summary. opa feeds Rego, so the type must keep a named MarshalJSON, as the tool-schema change did.
  3. Session facts instead of the session view.
    • Add a plain pctx.SessionID, and move the four ID-only plugins to it. Keep SessionSource, SessionHeader and ChainSession (cpex reads them; they come from attribution, Move agent and session attribution into its own package, callable by any listener #1498).
    • Give ibac, sparc and the router the one fact each needs:
      • ibac: the last intent text, keeping its distinction between "no session" and "no intent".
      • sparc: the latest inference request's messages and tools.
      • router: the server this agent's session last ran on. This must match serverOf's rule exactly (per agent, skipping aside turns, counting a captured unrouted row) and survive a restart, so it needs a fold in both the store and the archive. The store's existing inferenceHost fold uses a different rule. Owned here; Separate the inference-router's decisions from its plugin glue #1504 depends on it.
    • Each is maintained when events are appended, rather than rebuilt from a copied view. Some of this exists already: the intent pin and inferenceHost.
  4. Serializable records between plugins. Make the cost record a documented JSON shape, and read it through a decoder rather than a Go type assertion. This covers settle.Load/Amend as well as tool-prune's read; coordinate with Price a request from plain values instead of the plugin request context #1501, which changes settle's inputs.
  5. Not proposed here: freezing opa, static-inject or jwt-validation. Praxis has candidate equivalents, but freezing only pays off if a move is decided, so it isn't useful to Cortex now.

Design questions

  1. Should session facts be fields on pctx, or a small SessionFacts interface the store implements?
  2. Which of the three moves to facts first? ibac is the smallest (818 non-blank lines) and needs the simplest fact.

Done when

Evidence read at main @ e455a04 (2026-10-25); not built or run.

Activity

  1. added
    enhancementNew feature or request
    laptopThe local (laptop) tool: install, abctl, the numbers
    on Oct 25, 2026
  2. changed the title [-]Praxis prep: Make Cortex plugins portable to Praxis, one at a time[/-] [+]Decouple plugins from Go pipeline internals: parser test data, session facts, serializable records[/+] on Oct 25, 2026
  3. changed the title [-]Decouple plugins from Go pipeline internals: parser test data, session facts, serializable records[/-] [+]Decouple plugins from Go pipeline internals: session facts and serializable records[/+] on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestlaptopThe local (laptop) tool: install, abctl, the numbers

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions