Skip to content

Incremental teaching-table prompts and reducing prompt churn

This document describes how Plasm serves the Plasm teaching table (many-shot, symbol-tuned TSV examples) for HTTP execute and MCP execute sessions, and why that design reduces prompt churn for agents and humans.

Teaching medium: agent-visible context is always the TSV table (plasm_expr, one tab, Meaning), optionally prefixed by # comment contract lines and wrapped in a markdown fence by HTTP/MCP hosts. The legacy compact markdown transcript (;;-style blocks) is not emitted on the wire.

Goals

  1. Less redundant context — Avoid sending the full teaching table on every tool turn when the session’s catalog entry and seeds have not changed.
  2. Incremental graph exposure — Treat the CGS as a graph: ship teaching rows in waves as more entity types are needed, instead of always expanding to a large 2-hop neighbourhood in the first message.
  3. Stable symbolic indices — Keep e# / m# / r# / v# assignments monotonic: once assigned in a session, a symbol does not change meaning when new entities or capabilities enter the slice. Fields, params, and filters use catalog wire names (homograph wires disambiguated by syntax position). Relations use r#. Legacy opaque p# tokens are rejected at parse.
  4. Aligned parse + teaching table — Programs must parse with the same session SymbolMap the teaching TSV taught (e1.m3(…) resolves in-grammar; there is no pre-parse expand_* string pass).

Prompt churn” here means: repeated or oversized teaching text in agent context (duplicate full prompts on session reopen, multi-megabyte tables when only a small neighbourhood is needed, or shifting m# indices between waves). Those waste tokens, confuse models, and break trust in symbolic examples.

Problem (before this design)

  • Full dump — Rendering the teaching table for the union of 2-hop neighbourhoods around seeds produced large prompts even when the task only needed a few entity types.
  • Repeat sends — MCP plasm_context (open path) could return the entire teaching table again when the server reused an existing session (reused: true), unless the client omitted the body (we now omit the teaching block on reuse).
  • Index drift — A naïve rebuild of SymbolMap from a growing entity set can re-sort method keys globally, which would reshuffle m# values between waves. Incremental sessions instead append new (domain, kebab) and identifier bindings.

Design overview

FocusSpec::SeedsExact

Teaching-table slicing can use an exact entity list (no automatic 2-hop union). That list is the first wave of exposure: only those entity blocks appear in the initial teaching string.

Implementation: FocusSpec::SeedsExact and entity_slices_for_render.

TeachingExposureSession

A session-scoped structure in plasm-core (TeachingExposureSession) allocates:

  • e# — Order of first exposure of each qualified (registry entry_id, entity) pair. Colliding entity names across catalogs (e.g. github:Issue and linear:Issue) receive distinct e# symbols; teaching rows and surface filters always use the session registry entry_id, not bare CGS::entry_id from YAML fixtures.
  • m# — New (domain, kebab) capability pairs, sorted only among newly added pairs, then assigned the next free m indices.
  • r# — Declared relation navigation slots.
  • v# — Value-domain gloss rows (metadata only; never in programs).
  • Wire names — Fields, capability params, and query filters appear as catalog wire tokens in the teaching TSV left column (scoped under e# / m#).

Existing assignments are never rewritten. Rendering uses render_teaching_prompt_bundle_for_exposure; later waves pass emit_entity_blocks so only new entity blocks are appended (and the main “Valid expressions” preamble is omitted on those waves).

Teaching exemplar anchors (CGS binding surface)

Whether teaching rows should include an entity anchor exemplar (for example Entity($) / symbolic e# usage) must not be decided in the prompt layer by naming a transport (for example “GraphQL”). plasm-core exposes transport-neutral predicates on the capability’s mapping template:

  • template_domain_exemplar_requires_entity_anchor — true when the template needs an anchor for teaching examples: HTTP path template variables or a GraphQL operation variables block that binds an id (or equivalent single-entity key).
  • template_invoke_requires_explicit_anchor_id — used for expression pre-parse / shadow-invoke rules when an explicit anchor id is required (path vars or any GraphQL operation variable list), matching the compile path’s expectations.

CapabilitySchema::domain_exemplar_requires_entity_anchor and invoke_requires_explicit_anchor_id delegate to those helpers. Teaching synthesis consults the schema-level predicate (for example via path_vars_empty in prompt_render) so prompt synthesis stays free of GraphQL-specific conditionals.

When the cumulative slice includes structured string semantics, the preamble adds <<TAG heredoc rules in prompt_render: copy-pastable fenced text blocks show tagged form only. The only multiline/raw string form in path expressions is bash-inspired <<TAG + newline + body + closing line (trimmed TAG), with the same close optionally glued before ) / , / }.

Grammar note: The opener is << (two characters) plus a tag, not <<<. Legacy d<<< is removed—use <<TAG only (never << + newline alone).

$ and ~"text" (teaching only)

Teaching TSV rows use $ and ~"text" as fill-in cues, not values to copy into executable programs. The parser accepts bare $ as the string "$" (expr_parser); submitting e#~$ runs a real search for that character (e.g. Linear issue_searchtitle contains "$"), which often returns zero rows.

  • Never emit bare $ in plasm programs — substitute concrete ids, filter keys, or search strings from context (team list, prior bindings, user intent). plasm_run receives only run_ref (pcN or a page handle).
  • Full-text search rows teach e#~"text" (quoted meta-literal). Replace text with real terms, e.g. e2~"billing", not e2~$.
  • Search-only entities (no query capability, e.g. Linear Issue): there is no e#{} “list all”. Use scoped filters shown in the teaching table (e#{field=…}) and/or real ~"…" search text. Resolve filter values from the workspace (e.g. list Team first — do not assume doc-example keys like ENG).

MCP initialize teaches these grammar rules once; plasm_context waves return teaching TSV rows/symbols only and do not repeat the grammar preamble.

Execute session state (plasm)

ExecuteSession holds:

  • prompt_text — Cumulative teaching text (wave 1 + optional ## Expanded capabilities sections).
  • teaching_exposure — The [TeachingExposureSession] used for teaching rendering and session [SymbolMap] alignment (parse_session_line, wire-surface display via wire_surface_for_session_with_optional_exposure).
  • domain_revision — Increments each time more entities are exposed (wire field name; teaching-table revision counter).

Session identity (prompt_hash, session id) stays stable across waves; the hash is still derived from the initial prompt text for routing (see agent code paths).

MCP tools

  • plasm_context: Call first on each MCP connection. Pass session_mode: "new" once per workflow (or "extend" + logical_session_ref on later turns) and required intent. See mcp-session-reuse.md.
  • Semantic auto-seed hosts (default plasm-server / hosts with PLASM_DISCOVERY_SEMANTIC_AUTO_SEED=1): new and intent-only extend take intent only — seeds are rejected. The host routes seeds; ready mints/continues logical_session_ref + teaching TSV. Clarify / hard_miss do not mint a session.
  • Manual-seed hosts (auto-seed off): pass a non-empty seeds array of { api, entity } on new / extend.
  • Returns logical_session_ref as a stateless wire handle l_<token> for subsequent plasm / plasm_run calls (see mcp-logical-sessions.md).
  • On a fresh open (no live execute binding for that logical id), the primary api is the lexicographically first distinct catalog id among seeds — this keeps SessionReuseKey stable if the host reorders an equivalent seed set. Secondary catalogs in the same call are federated/expanded in lexicographic api order (after the primary), so multi-API open order does not depend on seed list order. Tool output returns delta-only teaching waves (no full prompt replay on federate/expand), while session symbol maps stay append-only. MCP _meta.plasm.continuity always includes stale_binding_recovered and new_symbol_space (and discard_cached_plasm_symbols when new_symbol_space is true) — when that flag is set, discard any prior e#/m#/r# (+ wire names) cached in the agent. Tenant MCP config scopes allowed APIs; a disallowed API fails the whole call. The teaching TSV contract teaches named p#=… / name=… slots for creates/updates; do not infer field meaning by permuting p# numerically after a new wave.
  • plasm: Pass logical_session_ref and program. Runs a plan-only dry-run using the session’s exposure map. Clean reads may auto-execute; reviewed / mutating plans mint run_ref (pcN).
  • plasm_run: Live execute by passing logical_session_ref and run_ref only. Do not echo program; MCP plasm_run does not accept wait, cancel, force, execute, plan_commit_ref, or page_handle. Expensive plans await server-side and return one terminal response. See plasm-long-operations.md.

MCP initialize teaches the plan/run split alongside entity/query grammar: author programs only for plasm, then pass the returned run_ref to plasm_run. Tool-model execute notes mirror the same MCP await-by-default discipline (tool-model-http.md).

Intent-scoped exposure (when plasm_context sets context_intent): capabilities on non-seeded entities still require lexicon overlap with intent. Each seeded { api, entity } always teaches that entity’s query / search / get surface (and primary_read when declared). Production waves use MutatorAdmit::IntentOnly: seeded create / update / delete / action require intent lexicon overlap (or appear in ranked_capabilities when the ranked gate is enabled). MutatorAdmit::AlwaysOnSeeds is test/benchmark overshow only. HTTP CreateExecuteSessionBody.mutator_admit defaults to IntentOnly; live open/federate/expand and exposure replay always pass IntentOnly. Updates / deletes / actions on non-seeded entities remain intent-filtered. See derive_intent_exposure_surface_batch.

Cardinality: many logical sessions per MCP transport (MCP-Session-Id); one active Plasm execute binding per logical session (see mcp_server.rs module docs).

Federated sessions (multi-catalog)

A single execute session (prompt_hash + session) can expose entities from more than one registry row (entry_id) without merging their CGS graphs into one artifact.

  • Prompt / symbolsTeachingExposureSession tracks which catalog each exposed entity name belongs to via entity_catalog_entry_ids parallel to entities; e# assignment, added detection, intent-surface filters, and federated teaching deltas key on (entry_id, entity), not bare entity names. Teaching rendering and the symbol map stay append-only (e# / m# / r# (+ wire names) monotonic within that session). Headings and tables can reflect (registry entry, entity) so the model knows which API each block refers to. Teaching TSV emission uses SymbolMap::entity_sym_for / ident_sym_*_for with the owning entry_id; unqualified SymbolMap::entity_sym returns a wire name when the same entity label appears in more than one catalog — agents must copy the e# from the row for that catalog block, not infer from Issue alone.
  • Execution — The agent keeps one CgsContext per entry_id (backend URL, auth, and its own CGS). FederationDispatch maps exposed entity names to the owning context; the runtime selects HTTP origin (and typecheck graph) per operation, not a single merged schema.
  • MCP — On manual-seed hosts, if an execute binding already exists and seeds include an entry_id not yet in the session, the server federates that catalog into the same session (additional teaching wave, same binding). Seeds for already-loaded entries produce expand waves. On auto-seed hosts, federation/expand follows intent routing (no client seeds).
  • HTTP — Primary flow is still POST /execute with one entry_id; extending with a second catalog may use the same federate path as MCP where implemented (see http_execute.rs).

Not in scope: global merge semantics for colliding entity names across catalogs — prompts are symbolic and (catalog, entity) disambiguates; sessions do not rely on a structural union of CGS.

HTTP parity

POST /execute creates sessions the same way (incremental first wave + stored teaching_exposure). There is no separate HTTP route for expansion in the minimal design; MCP plasm_context invokes the same expand/federate paths server-side.

HTTP execute also supports ?mode=plan, ?wait=false, ?force=true, and ?plan_commit_ref=pcN on live runs — and program bodies wait(oN) / cancel(oN) (plain handles, no l_<token> prefix) when no MCP plasm_context is present. See plasm-long-operations.md.

MCP: who orders discover vs execute?

The host agent (e.g. Cursor) decides which tool to call and when. The server surfaces plasm_context first in tool order and initialize instructions requiring it before other Plasm tools; it cannot fully enforce ordering. If the model skips search, you may see only plasm_agent::http_execute “execute expression” lines in logs — that means the client went straight to execute after (or without) a plasm_context open that might have happened in an earlier turn or session.

Observability: at INFO, plasm_agent::mcp logs discover_capabilities, plasm_context, plasm, and list_registry when those tools run, so a healthy flow shows one discover (or retry if incomplete) → plasm_context → plasm explicitly. Filter with RUST_LOG=plasm_agent::mcp=info (or info for the whole crate) to confirm.

Summary

Prompt churn is reduced by (1) exact first-wave teaching size, (2) append-only waves via plasm_context seed deltas, (3) no duplicate teaching table on reused opens, and (4) monotonic e#/m#/r# (+ wire names) so earlier examples remain valid as the session grows. Federation adds (5) multi-catalog sessions without merging CGS — same monotonic symbol stream, dispatch per CgsContext.