Prompt extraction migration¶
The migrated path composes the model-visible extraction directive and durable extraction separately. A directive tells the response model which operations to emit; MemoryExtractionService parses a separate extraction-model response and writes only validated records. Neither collaborator silently creates the other.
from symfonic import Agent
from symfonic.capabilities.memory import (
LegacyGraphOperationsAdapter,
MemoryExtractionService,
memory_capabilities,
)
from symfonic.capabilities.prompting import ExtractionDirectiveSource, PromptingCapability, extraction_contribution
directive = ExtractionDirectiveSource(
template_path=custom_template,
cadence=2,
scope_path=scope.path,
)
agent = Agent(provider, capabilities=[
*memory_capabilities(
store,
scope,
extractor=LegacyGraphOperationsAdapter(
MemoryExtractionService(extraction_model)
),
),
PromptingCapability(sources=[extraction_contribution(directive)]),
])
cadence=1 renders on every turn, cadence=N renders on turns divisible by
N, and cadence=0 omits the directive. For N > 1, pass a stable
session_id to Agent.run() or Agent.stream(); the counter is keyed by the
full scope and session, so one conversation cannot advance another's cadence.
The source requires a tenant scope and does not render on an unscoped turn.
For a scoped AgentHost, pass the composed scope's .path as shown above.
Custom templates retain MemoryExtractionSection validation.
When the response model emits a complete <MEMORY_EXTRACT> or
<GRAPH_OPERATIONS> envelope, the public Agent removes that envelope from its
result and stream. MemoryExtractionService persists an inline operation only
when it uses its native action/properties classification schema; rejected
tagged operations fall back to the configured extraction model. The bundled
legacy templates use type/payload, so compose
LegacyGraphOperationsAdapter explicitly around MemoryExtractionService for
those templates. It translates only upsert_node, set_trigger,
create_event, and register_skill into native record operations, validates
the required layer and documented payload fields, and derives classification,
scope, and origin from the native request. It rejects retract_node, mixed
schemas, malformed payloads, and supplied scope or provenance fields; their
envelopes remain hidden from the user and invoke the configured fallback.
The adapter does not issue arbitrary graph writes or implement legacy
retractions. A custom template can instead emit the native schema directly.
The total PromptBudget remains a compiler-wide ceiling. When a deployment
also needs the legacy regional controls, compose them explicitly with
RegionalPromptPolicy. Its HMS boundary is pinned and measures the authored
HMS section with the legacy three-characters-per-token estimate. In warn
mode it sends the whole section and reports the first overflow; in strict
mode it reports before failing closed, before the provider receives a request.
The observer receives only (region, requested_tokens, limit), never prompt
bytes. The JIT manifest source has its own four-characters-per-token cap, so
one regional cap cannot consume the other.
from examples.migration_parity.regional_prompt_budgets import build_agent
agent = build_agent(
provider,
hms_state=deployment_hms_state,
domain=domain,
jit_manifest=("search: find records", "calendar: schedule meetings"),
on_hms_budget_exceeded=record_overflow,
)
The composition refuses a non-zero JIT budget without a manifest and refuses a
manifest unless the caller explicitly declares the JIT strategy. It projects
only DomainTemplate.description into the HMS source and applies the full-HMS
or JIT member of domain_description_max_chars deliberately. The legacy JIT
template itself is not adapted by this composition, so its exact body and
query-keyword filtering remain a migration gap; this does not approve a
blanket transition for Issue #187.