Repository navigation
Description
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-praxisconverter 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):
- 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 asfilter_metadatastrings orstructured_metadataJSON. - 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.
InferenceExtensionandMCPExtensioncarry JSON tags, but have no schema or fixtures.MCPExtension.Paramsand.Resultaremap[string]any(extensions.go:102-103). Tool schemas were already moved toRawJSONfor the same reason.
Only three plugins read session history:
- ibac:
pctx.Session.LastIntent() - sparc:
pctx.Session.InferenceRequests() - inference-router:
view.Events, and then the on-disk archive throughp.history.Earlier, up to 2,000 events (plugin.go:680-699), so a session resumed after a restart still finds its server
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.Loadand rewrites it throughsettle.Amend, also Go values (settle.go:785-818). - The other direction already decodes:
settle.Avoidedreads 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/): PPEidentity_jwt. Mainly used by the cluster sidecars, not the laptop. - static-inject (283): Praxis's
headersandcredential_injectionfilters.
Proposal
- 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.
- Write down the parsed-request contract. A JSON schema for
InferenceExtension,MCPExtensionandA2AExtension, generated or checked from the Go types, linked fromdocs/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/ResulttoRawJSON(alsoRPCID anyandMCPError.Data any). Consumers: cpex, ibac, lineage, opa, sparc and the session API's summary. opa feeds Rego, so the type must keep a namedMarshalJSON, as the tool-schema change did.
- Move MCP
- Session facts instead of the session view.
- Add a plain
pctx.SessionID, and move the four ID-only plugins to it. KeepSessionSource,SessionHeaderandChainSession(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 existinginferenceHostfold 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.
- Add a plain
- 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/Amendas well as tool-prune's read; coordinate with Price a request from plain values instead of the plugin request context #1501, which changessettle's inputs. - 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
- Should session facts be fields on
pctx, or a smallSessionFactsinterface the store implements? - Which of the three moves to facts first? ibac is the smallest (818 non-blank lines) and needs the simplest fact.
Done when
- The parsed-request types have a schema the parser test cases (Turn the MCP, A2A and inference parsers into libraries, with saved real-traffic test cases #1503) check against.
- ibac, sparc and inference-router read facts, not views; no plugin reads
Session.Events. - The router's fact gives the same answer as today after a proxy restart.
Store.Viewis no longer called per request in any listener.
Evidence read at main @ e455a04 (2026-10-25); not built or run.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNew/ToDo
Activity
[-]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[/+][-]Decouple plugins from Go pipeline internals: parser test data, session facts, serializable records[/-][+]Decouple plugins from Go pipeline internals: session facts and serializable records[/+]