Skip to content

Changelog

User-facing changes to symfonic-core. The format is based on Keep a Changelog and the project follows Semantic Versioning.

This is the curated public changelog — it covers changes relevant to users of the published package. Fine-grained internal release notes are maintained separately by the maintainers.

[Unreleased]

Private 11.4.0 is published; this source tree declares the private 11.5.0 release candidate. This status does not announce a public PyPI release. A later main push can publish private artifacts and, after the exact-artifact and adopter gates, deploy public documentation.

Private 11.5.0 engine-free release candidate

  • src/symfonic/agent/engine.py is physically absent from the source tree, wheel and sdist. Native Agent and AgentHost are the v11 composition route. SymfonicAgent(...) and AgentBuilder.build() remain importable but raise EntryPointRemovedError; callers that need those constructors must pin symfonic-core==9.12.*.
  • symfonic.migration.agent_from_config requires explicit auto_consolidate=False. Legacy automatic consolidation, including its PostgreSQL behavior, is not carried forward. Native QUICK consolidation requires explicit AgentHost opt-in.
  • The historical fourteen-condition retirement gate remains unfulfilled. This private engine removal does not certify full legacy parity or complete that gate. The historical feature inventory retains its original scope.
  • This candidate targets the private Gitea registry only. Public PyPI publication requires a separate stable tag; public docs may deploy after the private release and documentation gates.

Added

  • Native Agent accepts explicit result_policy=ResultPolicySettings(...) for tool-result compaction. Import settings from symfonic.agent.result_policy; compaction stays off by default. Oversized results become provider-visible recall stubs after the configured recent-result window, while AgentResult.tool_calls and the transcript retain full content. Native Agent accepts enabled, size_chars, and keep_last_n; it refuses nondefault values for the other settings. The built-in recall_tool_result can recover a result only within the same run; handles do not persist across runs. See The Simple Agent (CORE-461).

  • symfonic.capabilities is now a declared public package: the names its __all__ exports (CapabilityContribution, Phase, StageDescriptor, no_change, ...) are the supported way to write your own capability. A project generated by symfonic init now lists symfonic.capabilities in the allowed modules of its own public-API test. Declaring the package root does not make its submodules public. See "Write an adopter capability" in the capability migration guide (CORE-25).

  • symfonic.core.providers.MultiProviderRouter is now a sanctioned import, by exact name, alongside the concrete providers it routes to: symfonic doctor reports importing it as SANCTIONED-UNDECLARED (informational, no action). The rest of symfonic.core.providers stays undeclared (CORE-25).

  • symfonic.capabilities.human.current_run_binding(...) binds a pause to the turn that is pausing, using public imports only: pass binding=lambda: current_run_binding(scope=..., session_id=...) to human_interaction. It fills run_id/root_run_id from the running turn, accepts no caller-supplied run id, and raises InteractionConfigurationError outside a turn. Resume checks are unchanged. See the capability migration guide.

  • symfonic.platform exports the framework's pause signer classes, EnvelopeSigner, InvocationPin, Keyset, KeyState and PinEnvelope (the same classes as before, now on a public package), so a human_interaction composition with current_run_binding, including a token codec that serializes the signed envelope, needs no private import. You supply the signing key and the token format; a rebuilt envelope is verified before a pause is honoured. current_run_binding fills only the run ids: the session and scope you bind are yours to check. Covered: pausing and resuming once in the same process; not a fresh process or resume_stream.

  • A PostgresConversationLog used on its own, without raw PostgreSQL v1 memory, can now prove that its database still holds every acknowledged transcript forget: pass erase_anchor=PostgresEraseAnchor(anchor_pool, memory="<name unique to this database>") and receipt_key=, install the log (ensure_schema() or scripts/migrations/core171_conversation_erase_log.sql) and run log.proof.genesis(owner_conn) once. A physical copy from before a forget refuses BEHIND before any read, turn, append, fork or export; a logical restore re-opens only through postgres_conversation_restore. Breaking: without an anchor such a log refuses every read and write once its erase generations are installed (CORE-171).

  • Kind-aware Mongo memory can be forgotten: MemoryAdminService(store, operation_journal=journal).forget(scope) erases the scope and everything below it in one transaction. Positional row migration now runs on Mongo too, unless that forget could not erase the result afterwards. A migration quarantines an old row instead of moving it under a scope whose forget was recorded, and refuses while a quarantined link would remain touching a memory it moves, until that link is deleted (delete_quarantined_edges= for a link the plan quarantines). On PostgreSQL, a forget from before memory_erase_generations was installed was not recorded, so a migration cannot prove an old row's owner was never erased: any plan that would move a row there refuses, and only quarantine-only plans run. Apply and rollback refuse a plan that was edited after planning, and apply moves only the rows it re-reads from the store. A migration that moved a row to a kind-aware owner cannot be rolled back. A quarantine-only plan rolls back only on the in-process store, and only when it proves no forget or delete has run in the tenant since apply (no forget reaches a quarantined row, so a restore would otherwise bring back what that forget erased); on PostgreSQL and Mongo rollback refuses. On the in-process store, apply and rollback refuse inside a graph transaction you opened, where a forget from a joined task could otherwise slip in before the re-key. A PostgreSQL tenant with any clearly owned row still cannot plan at all. See Rows written under positional scopes (CORE-171).

Changed

  • Breaking since private 11.4.0: create_graph_router, create_graph_maintenance_router, create_memory_create_router and create_tenant_privacy_router still import but raise EntryPointRemovedError when called, before reading the agent. Mount the same routes with create_kernel_agent_router and its graph=, edge_delete=, graph_audit=, *_create= and privacy= ports; the error lists the ones each factory needs. POST /edges, POST /memories/bulk-delete and POST /memory/prune have no native replacement. To keep the old factories, pin symfonic-core==9.12.*. See legacy HTTP factory migration (CORE-25).

  • The native HTTP router now refuses to start in production without tenant authentication. create_kernel_agent_router raises a RuntimeError when any of SYMFONIC_ENV, APP_ENV, ENVIRONMENT or NODE_ENV is production (or prod) and its scope resolver has no verifier, instead of trusting the X-Tenant-ID header. Pass HeaderScopeResolver(verifier=...). To accept the risk explicitly, pass HeaderScopeResolver(allow_insecure_prod=True); the ALLOW_INSECURE_PROD environment variable has no effect on this router. A custom resolver must return symfonic.platform.AuthPosture from posture() to be accepted in production.

  • Breaking in private 11.3.0: a resumed or recovered run whose paused run is no longer alive in the process now refuses before any model call, tool call or token claim, unless it carries the persisted proof described next. Its history cannot otherwise be proved current after a forget. The token stays unspent. New, unnamed runs are unaffected.

One case resumes after a restart: an ask_user pause taken by an Agent root run that composes no memory writes a sealed proof beside its turn state, and a new process using the same checkpoint store and signing key redeems it once through AgentHost.resume or resume_stream, provided the resuming agent also composes no memory. Pauses taken or resumed with memory, delegated or child pauses, synthetic bindings and missing or altered proofs still refuse. If you need cross-restart resumption for those, stay on 9.12. See the 11.x transition guide (CORE-194, CORE-386).

  • Breaking in private 11.3.0: native PostgreSQL memory needs an erase anchor database in a separate cluster, passed as PostgresNativeEraser(..., erase_anchor=PostgresEraseAnchor(pool, memory=...)). With it, memory restored from a physical backup, snapshot or replica refuses rather than serving content you had forgotten, and a forget reports success only once the anchor has recorded it. See the memory persistence guide (CORE-171).

  • A native PostgreSQL subtree forget whose content exceeds 1,024 rows in a table no longer has to refuse. Once the operator installs chunked content erasure and the host composes a PostgresNativeEraser, the forget returns ForgetInProgress; from that moment every content read hides the whole subtree, a flush or discard that would select its pending rows refuses, and every write under it refuses, and forgetting the same scope again erases it in chunks until a LifecycleReceipt with no ids reports it complete. Without the installation the refusal is unchanged. Updates and deletes of native graph rows now also refuse under any erase in progress. See the memory persistence guide (CORE-199).

  • The native PostgreSQL subtree reads and subtree forget now document their scan bound contract: a row budget, a 1 s time limit and an index-ordered plan always hold, and each range read visits at most 2,114 shared buffers only while the operator keeps dead and retained row versions and index churn within two stated preconditions. Subtree forget's EXCLUSIVE lock, which stalls writers of every tenant, lasts at most its 1 s deadline plus a 0.25 s commit grace. See the memory persistence guide (CORE-199).
  • Breaking in private 11.3.0: SymfonicAgent(...) and AgentBuilder().build() raise EntryPointRemovedError before anything is built, with no compatibility shim. Build one agent with symfonic.Agent, host agents with symfonic.platform.AgentHost, and carry an existing FrameworkConfig over with symfonic.migration.agent_from_config. symfonic-core==9.12.* is the newest public PyPI line on which both still construct (private 11.0 to 11.2.6 did too); it predates the 11.x features. See Moving to 11.0 (CORE-388).
  • BREAKING (since private 11.4.0). LegacyTranscriptReaderFactory composes a transcript reader without constructing SymfonicAgent; its model_provider argument is now optional and unused. From private 11.4.0, a direct read of an absent checkpoint or one without a matching native owner raises the same generic TranscriptReadRefused(ConversationRefused) with contract direct_transcript_read.v1, Agent/AgentHost migration guidance and the 9.12 legacy pin. Only a valid native empty conversation returns []. Earlier private candidates returned [] for unsupported checkpoints, where 11.2.6 returned legacy rows. The reader raises ConversationRefused for an ambiguous scope (sub-tenant "_", or ":" in the tenant or sub-tenant). There is no compatibility shim. The authority still authorizes first; a compatibility reader now also refuses a session discarded between authorization and lookup. See direct transcript reads and conversation persistence (CORE-22, CORE-391).
  • A memory-hydrating SymfonicAgent turn served by the kernel route now counts an access on every memory it delivered to the model. Legacy counted only its keyword hits, so access_count can grow faster than it did on the legacy body. Native Agent memory accounting is unchanged.
  • audit_hms, audit_hms_sync and AuditRunner in symfonic.diagnostics are deprecated. Each now warns with a DeprecationWarning that points you at symfonic doctor PROJECT_PATH --scope <tenant path> (or symfonic doctor --offline --config <file>), and still returns the same report as before. The doctor is the supported path, but neither command repeats the audit's five checks: without --offline the doctor lists each as UNKNOWN, and with --offline it omits them. See Capability architecture migration (CORE-389).
  • An Agent built with symfonic.migration.agent_from_config and served by create_kernel_agent_router now answers POST /chat with the synthesized summary alone when a turn used up its round budget and synthesize_on_empty_final=True produced one. When the turn's last round spoke beside its tool call (so synthesis did not run), it answers with that round's visible text, as the legacy body did. That holds for any Agent composing TerminalSynthesisCapability, however it was built; another capability using the terminal_synthesis name does not count. Any other round-limited turn, and any round-limited turn of an Agent without synthesis, is still a 502. A config the boundary refuses now answers the chat and stream routes with 502 {"detail": "the turn failed"}, with the refused settings in the server log. self_reflection, reflection_prompt and skill_auto_approve are accepted, with no effect, when auto_consolidate=False. See From an existing FrameworkConfig (CORE-235).
  • agent_from_config refuses synthesize_on_empty_final=True beside a TerminalSynthesisCapability you composed yourself with its versioned UnsupportedFrameworkSettingError, still a ConfigurationError, instead of the Agent's own unversioned error, so an AgentHost route answers it with the same 502 as any refused setting, before any provider, memory or tool call. See From an existing FrameworkConfig (CORE-235).

Added

  • A symfonic.Agent can narrow the tools it offers from procedural memory with procedural_router: only a stored skill that matched the request narrows the palette, and fallback or empty-cue rows keep every registered tool. See Guide 6.
  • Fallback and dispatch evidence journals now record which installed symfonic-core version wrote each row (package_version), next to the release_version label you choose. The version is recorded only when a non-editable install provably owns the running code and its version label, unchanged since the process started; source checkouts, editable installs, in-place upgrades, edited version labels and workers forked from a process that had already imported Core write rows without it. The deployment report counts rows per version and stays incomplete when one window mixes versions, or when you declare package_version in the manifest and a row does not match it. Rows written by earlier versions are still read. A zero-fallback verdict now requires the manifest to declare package_version and every row in the window to match it; otherwise the verdict is incomplete_evidence and package_version_binding says why. Zero also requires the manifest to declare the fallback journal files (expected_fallback_journals), because only that journal records a legacy ask_user guard refusal as a fallback; without it the verdict is incomplete_evidence with fallback_capture_undeclared. Each declared file must hold fallback rows only and be declared once; sharing it with another journal or another replica also withholds zero. See Deployment-attributed fallback evidence (CORE-24).
  • recall_policy_for lets a native Agent that composes opt-in prospective triggers keep the stored reminder out of generic recall, so only a satisfied trigger's task reaches the prompt. Nothing changes unless you use it. Both prospective create routes also store the trigger's kind now. Trigger selection does not use it; the SymfonicAgent FastAPI memory list and tenant export, which return stored properties, now show it for new rows, while the native transport's list and export do not. See Prospective triggers on a native Agent (CORE-97).
  • A reviewable migration path for memory rows written under the old positional (three-level) scopes. You declare which scope kinds your app used at each depth; rows whose owner that declaration identifies exactly move to kind-aware scopes, and rows whose kind cannot be recovered are quarantined: no agent reads them, no forget removes them, and only an explicit, named deletion does. Plan and apply are separate steps, and apply refuses if the store changed since the plan was reviewed. It runs on the in-process graph store, on PostgreSQL (quarantine-only plans; see above) and on Mongo, where apply opened once kind-aware Mongo memory could be forgotten (see above). A move is one-way. Rollback undoes a quarantine-only plan only on the in-process store, and only where the store proves no forget could have covered the rows since; on PostgreSQL and Mongo it refuses. Apply, rollback and deletion need an audit logger and record each row's previous owner before changing it. Kind-aware agents never fall back to reading positional rows, and any part of your app still using positional scopes keeps writing rows for a later plan. See the memory persistence guide (CORE-171). A scope kind your app chose that happens to begin with symfonic.memory. is migrated like any other explicit kind; it is no longer skipped by the plan or treated as an already quarantined row.
  • The HTTP memory routes can follow the scope your agent actually uses. Pass memory_identity to create_kernel_agent_router and reading or deleting one memory reaches the row your agent wrote; routes that cannot serve kind-aware scopes yet answer 501 instead of showing different rows, and a declaration that fails or names another tenant answers 500. See the memory persistence guide (CORE-171).

Fixed

  • symfonic doctor no longer reports MultiProviderRouter as a deprecated import, and the capability migration guide no longer lists it as replaced by ModelResolutionService. The router is unchanged and is not deprecated: it dispatches each call to the provider for its model, and ModelResolutionService chooses which model configuration a call uses. The two work together. See the D1 row in the capability migration guide (CORE-25).

  • On graph-only PostgreSQL v1 memory, a forget now refuses with MemoryContractError when the memory_erase_generations table is not installed. It refuses before it deletes anything, creates an erase job or records a transcript erase intent. The intent check holds for a table that is missing when the forget starts. If the table is dropped after that check, the intent can stay committed while memory refuses. In that case the transcript stays fenced until a retry, after the table is reinstalled, completes the forget. An erase job already in progress refuses its next step until the table is back. The eraser's role setup and attestation also require the table. Before this fix such a forget completed without recording an erase generation, so an Agent turn that had recalled the content could write it back. See Memory persistence (CORE-171).

  • On graph-only PostgreSQL v1 memory, an Agent turn that recalled memory before a forget can no longer write it back after an operator drops and reinstalls the memory_erase_generations table. It now refuses with the stale-turn error, even if PostgreSQL gives the new table the old one's OID. Turns that start after the repair are unaffected. Upgrade step: re-run scripts/migrations/core199_memory_erase_generations.sql once, as the owner of memory_nodes. Until then, turns and forgets over that memory refuse. The re-run keeps every recorded generation. After restoring from a dump or pg_upgrade, turns and forgets refuse and the script refuses to re-run: re-open the database only with the restore procedure in the memory persistence guide, which refuses a backup taken before a forget. Physical backups and replicas are not detected, so a forget is not guaranteed to survive restoring one (CORE-171).
  • A kind-aware Mongo Agent turn that started before a forget of its own scope, or a scope above it, no longer writes into the forgotten scope: the turn fails and publishes nothing. When you upgrade, a turn started on an earlier build fails when it tries to publish memory, whether or not anything was forgotten. Retrying it with the same invocation id does not repair it; run it again under a new invocation id. Workers still on an earlier build do not apply this check, so the protection holds only once every worker that publishes Mongo memory for the tenant is upgraded. This change is unreleased. See Rows written under positional scopes (CORE-171).
  • On graph-only PostgreSQL v1 memory composed with memory_capabilities, a turn started with a parent or root run id that is no longer running in this process (for example by a worker that received the ids from an earlier turn) is refused before the model runs, and so is every write made inside it, at any scope. Before, it started as a new turn and could write data a completed forget had erased. See the memory persistence guide (CORE-194).
  • Graph-only PostgreSQL memory composed with memory_capabilities now refuses three ways a turn could carry forgotten data past an admin forget. Passing history to Agent.run or Agent.stream fails before anything runs, because a transcript you hold says nothing about which forgets it predates; use the composition's own conversation history, or history=(). A MemoryCapability built by hand over that memory, or over any wrapper or subclass of a store on a kind-aware scope, fails when it is built; compose it with memory_capabilities. And a turn whose stream is stepped from different tasks, or alternately with another turn, keeps its own record of where it started. The first two are breaking for deployments that relied on them. See the memory persistence guide (CORE-194).
  • Agents whose memory keeps scope kinds now work when the turn states its own scope. Before, such a turn failed while saving memory, and recalled memory never reached the prompt. A turn is refused when it states a scope of the other kind of identity than its memory was composed with, for example a typed scope for memory composed with a positional scope (CORE-171).
  • With memory_identity, listing memories over HTTP now shows the rows your agent wrote, instead of answering 501. See the memory persistence guide (CORE-171).
  • A RetrievalPolicy that leaves a memory layer out of enabled_layers is now honoured by the built-in spreading activation too: a memory in that layer is no longer recalled as a neighbour of one that is enabled, nor is anything reached only through it. The defaults enable every layer, so a deployment that never narrowed the set sees no change. A custom activation is not bounded this way. See the capability migration guide.
  • With a conversation window (recent_turns), recalled memory no longer repeats the turns that window already shows, when the window's source reports each turn under the same turn_id Symfonic stored it with (the run's run_id). The built-in native conversation history does; a custom window source that returns its own IDs gets no suppression, and duplicates can still appear. A window turn too long to fit the memory block's size limit is not shown, so its recalled copy can still appear. Memories from earlier turns, and facts, are not removed by this change, but they are not guaranteed to appear: the duplicate is removed only after retrieval has picked its results, so a top-ranked duplicate still uses one of retrieval's result slots and can push out an older, different memory. This is verified for the normal memory block with no custom compressor. If you configure a HydrationPolicy compressor, it runs after duplicates are removed and can rewrite or drop window lines, so there is no guarantee of duplicate removal or of the original wording after custom compression. Recalled memories stay in relevance (retrieval rank) order, not time order; showing conversation-window turns oldest first is a separate requirement (CORE-65) that this change does not address. See the capability migration guide.

Changed

  • Native PostgreSQL subtree reads now count expired Working memories toward the 1,024-row budget instead of scanning past them (subtree forget already counted them). A native subtree read over more than 1,024 stored rows, expired ones included, now refuses with MemoryContractError and returns nothing; expired memories still never appear in results. Subtree reads and forgets now use an index-ordered plan under both custom and generic plans, where they could previously fall back to a full scan and sort. This is not a bound on physical reads, which still depend on PostgreSQL's dead row versions. See the memory persistence guide (CORE-199).
  • Graph-only PostgreSQL memory with kind-aware scopes and an attested eraser can now delete a single record with delete_record, directly or through MemoryAdminService. Only a published record stored at exactly the given scope is deleted, together with its claim history and the edges its own scope, its ancestors or its descendants own; anything else returns None and changes nothing, and a record that still exists inside a scope being forgotten refuses. A delete is not a forget: a turn that recalled the record earlier can still write it back. This replaces the note below that point deletes stay refused on that setup. See the memory persistence guide (CORE-194).
  • Public graph-only PostgreSQL memory with kind-aware scopes (next entry) now also works when the agent connects as a separate, non-owner database user through PostgresGraphRuntime(pool, owner_login=..., eraser=...). await graph.ensure_schema() makes no schema changes: it checks that this user has exactly the data grants it needs and owns nothing, and that the eraser is attested for it, then the agent can write, publish, discard and forget as with PostgresGraphBackend. If any check fails, startup raises and composition refuses before any Agent exists. A subclass of PostgresGraphRuntime is still refused. See the memory persistence guide (CORE-194).
  • Public memory on raw graph-only PostgreSQL with kind-aware scopes can now write, publish, discard and forget, but only with a PostgresNativeEraser attested for that runtime. await graph.ensure_schema() (or graph.attest_eraser()) checks that the eraser logs in as a different database user, holds exactly its grants, and erases the same graph the runtime reads; otherwise startup raises EraserNotReady and composition refuses before any Agent exists. Check graph.eraser_attestation to see whether a runtime is attested. The check is not continuous: if the eraser's grants are revoked later, the next eraser operation that fails withdraws the attestation and writes refuse until you attest again. MemoryAdminService(store).forget(scope) without a conversation log now erases on this setup and returns the same receipt or ForgetInProgress; with a conversation log it still refuses. Point deletes, vector pairing, conversation, consolidation, summary and episodic telemetry stay refused here. This replaces, for this setup, the earlier note that public graph-only PostgreSQL admission stays closed. See the memory persistence guide for the PostgreSQL backend (CORE-194).
  • MemoryAdminService(store, conversation=log).forget(scope) now also works on that setup when log is a PostgresConversationLog, replacing the note above that it refuses with a conversation log. On the store's own pool, memory and transcript are erased together or not at all; an erase too large for one transaction refuses without changing anything. On a separate pool, memory is erased first and the transcript only once memory is fully gone. Until then, reading, appending to or forking an affected conversation refuses, and a call interrupted between the two steps is finished by calling forget again. Calling the log inside your own transaction on its pool requires READ COMMITTED; any other isolation level refuses. Run log.ensure_schema() after upgrading. Resuming a paused human interaction is refused for an Agent composed over this memory (with or without the conversation log) until pause checkpoints record the erase generation: host.resume raises MemoryContractError before the model runs, and the pause token stays redeemable. Keep the conversation tables in one schema on the connection's search_path: if two schemas there both hold them (for example public and your own), every read, append, fork and forget on the log, and every fenced memory write checked through that connection, refuses until one copy is removed. memory_capabilities(store, scope, conversation=NativeConversationHistory(log, ...)) now composes an Agent on this setup too, replacing the note that it is refused. A turn that began before an admin forget finished cannot write what it read back into memory or the transcript afterwards: it fails with MemoryContractError and writes nothing. Turns that start after the forget run normally, and this holds for writes a tool makes through the same store during that turn. Consolidation, summary and episodic telemetry stay refused. Do not set generations on the history yourself; it is refused. Breaking: once a forget has covered a scope, a bare store.write, flush or discard into that scope is refused, including from a task that drops its context or outlives its run. A tool or background task writes through turn_writer(store), which it must create while the turn is running. The guarantee does not cover PostgresGraphBackend methods or your own SQL. This now also holds for a memory-only forget (MemoryAdminService(store) without a log), with the transcript in a different database, and across restarts. Run scripts/migrations/core199_memory_erase_generations.sql as the role that owns memory_nodes (not a superuser that doesn't own it; the script refuses), then your role setup, before starting any runtime; until you do, Agent turns over this memory refuse to start, while forgets still complete. The table must stay a regular (not UNLOGGED) table, and PostgreSQL 16 is the supported server version: on another, the check may refuse a correctly installed table. Restart every process on the new release before relying on the guarantee: older processes do not check it. Breaking: MemoryAdminService.stage, publish and discard now need a running Agent turn that read this memory. Inside one they refuse a scope whose forget (of it or a scope above it) the turn's own start did not see; outside any turn, or in a turn that read no memory, they refuse. Once an Agent is composed over a store, a bare store.write, flush or discard through it outside a running turn is refused in every scope, so a task that drops its context cannot itself copy what a turn read anywhere else; write through turn_writer(store) from inside the turn. A new Agent turn such a task starts is held to the turn that spawned it, so after the forget it refuses too. That includes a task created while the event loop's task factory was replaced, or built directly with asyncio.Task: it is held to every turn that may have been running when it was created. A loop whose task factory wraps each coroutine (for example Sentry's asyncio integration) no longer makes every ordinary request look like such a task: each request is held only to its own turn, so one tenant's forget does not refuse another tenant's live turn. This does not extend to a task or event loop that already existed and receives the turn's data through a queue, a global or a callback, to work scheduled with loop.call_soon or run_in_executor without the turn's context, to other threads with their own event loop, or to other processes, whose new turns count as new. A forget of a scope also stops background work started from a turn that read it from writing anywhere, including scopes the forget did not cover: restart that work from a new turn. A turn whose scope has more than 1024 forgotten scopes below it refuses to start rather than check fewer. Writes through a store no Agent was composed over in this process, through another pool, or from another process are checked only against the scope they write and the scopes above it: content recalled from a forgotten child scope can still be written to its parent that way. See the memory persistence guide (CORE-199).
  • With lazy_tooling=True and a procedural memory layer, the agent now narrows the tools it offers only when a stored skill matched the request. When the layer answers with fallback or empty-cue rows instead, every registered tool stays available; previously those rows also narrowed the palette. A procedural layer that cannot report why a row was returned routes the turn to the legacy path instead. An agent with auto_hydrate or auto_consolidate on and no registered tools is no longer routed to the legacy path on account of lazy_tooling, including with the HMS prompt disabled, no tool manifest, no procedural memory layer, or a procedural layer that cannot report why a row was returned; other settings can still route the turn to the legacy path. On the native route it reads no procedural memory for tool routing and records no tools.routing_unavailable count, since there is no palette to resolve; memory hydration is unchanged. When tool routing is unavailable, the tools routing stage now records that count only if the turn's compiled plan offers a tool. That also covers a SymfonicAgent with lazy_tooling=True, memory off and no tools, and an Agent composed with ToolsCapability(routing_unavailable=...) and no tools. A turn that offers any tool keeps the count, whatever registered or count_unavailable declares. An agent with registered tools whose routing prerequisites are missing still routes to the legacy path. Its recorded fallback reason now says that tool routing was not proven for the turn, instead of naming a tools cutover switch that does not exist. With memory off, that agent is unchanged; see lazy tooling (CORE-227).
  • A SymfonicAgent turn served on the native route now keeps its memory effects. AgentResponse.memory_entries_used reports how many memories hydration recalled instead of 0; for a reply carrying a single memory block or trailing bare {"ops": ...} object, extracted_ops lists the operations it carried, including ones that were refused or, with auto_consolidate off, not filed. The stream done chunk carries both, and for a single trailing bare {"ops": ...} object reports the same operations as run even though streamed text is not rewritten and still contains it. A reply that mixes a memory block with a trailing bare {"ops": ...} object reports the block's operations only and files only those of them that are accepted; the trailing object is neither reported nor filed. This applies to every SymfonicAgent run and stream turn on the native route, with or without registered tools; the FastAPI SSE streams still report 0 and [] on their done event. run strips a trailing bare {"ops": ...} object and citation tags from final_response as the legacy path does. With auto_consolidate on, the facts in a MEMORY_EXTRACT/GRAPH_OPERATIONS block, or in a trailing bare {"ops": ...} object, are filed to memory instead of being stripped and dropped; operations that cannot be filed are logged, and one refused operation no longer prevents the others in the same reply from being filed. An Agent built with memory_capabilities and an extraction model is unchanged. No extra model call is made when a reply has no block. See consolidation (CORE-227).
  • compose_legacy_defaults no longer refuses lazy_tooling when no store and no memory row are requested. Without memory the row cannot activate, so the agent offers every registered tool, as legacy does. With memory it still refuses; see legacy default parity (CORE-20).
  • Generated projects now refuse SYNTHESIZE_ON_EMPTY_FINAL=true at configuration load instead of silently ignoring it. To summarize an empty text run(), compose TerminalSynthesisCapability() through build_agent(extra_capabilities=...); see the 11 transition guide (CORE-235).
  • PostgreSQL Agent memory now publishes a turn's facts only while the write that stored them is still committed and unchanged, rechecked when they are published, including on opt-in staged activation. If that write was removed or altered in between, activation raises SourceWriteRefused (a MemoryContractError), nothing is published, and retrying refuses again. Before upgrading, let in-flight PostgreSQL Agent turns finish, including staged turns not yet activated; the check applies once every node runs this version. See the memory persistence guide for the PostgreSQL backend (CORE-199, partial).
  • Opt-in staged PostgreSQL activation now publishes only staged rows whose content equals what READY sealed. A rolled-back activation raises instead of returning a non-ACTIVE status, and the turn stays READY with nothing published. StagedContentRefused means a staged row was removed or rewritten, and retrying refuses again. StagedActivationRolledBack means a transient failure, such as an embedding timeout, a cancelled statement or a lock wait over 5 s, and the same activation is safe to retry. Both are MemoryContractErrors. Staged PostgreSQL remains restricted to disposable test databases; see the memory persistence guide for the PostgreSQL backend (CORE-339a).
  • Native PostgreSQL storage gains a private eraser for a subtree whose claim history is too large for one transaction. No admitted composition reaches it yet: public graph-only PostgreSQL admission stays closed, so a public deployment still gets the refusal described below. Where a host composes it, a PostgresNativeEraser runs on its own database login and graph schema, which startup verifies, with a secret receipt key of at least 32 bytes; forget then returns a ForgetInProgress with a job id once the content is gone, and forgetting the same scope again finishes the job, after which it returns the usual receipt. Without an eraser the refusal is unchanged, and an admin forget whose conversation log shares the memory store's PostgreSQL pool still refuses it, changing nothing. See the memory persistence guide for the PostgreSQL backend (CORE-199).
  • Public graph-only reads on native PostgreSQL scopes (retrieve, inventory_records, get_record) now run under the 1 s native deadline. A read that cannot finish in time, including one over a high-volume owner, raises MemoryContractError and returns no partial rows. There is no pagination fallback. Legacy v0 scopes are unchanged (CORE-199).
  • Native PostgreSQL flush and discard refuse until an operator installs the partial index idx_nodes_pending_scope_c with scripts/migrations/core199_pending_index.sql. Startup never creates it. See the memory persistence guide for the PostgreSQL backend setup. Public native lifecycle admission stays closed.
  • Native PostgreSQL flush and discard of more than 1,024 pending rows now complete in one transaction instead of refusing. The 1 s deadline still applies: a larger batch refuses and changes nothing.
  • Native PostgreSQL discard, on the batched graph-only path and on the row-at-a-time path, now refuses and rolls back until an operator installs the index idx_pending_claims_record_c with scripts/migrations/core199_claim_record_index.sql. Native cascading node deletes refuse and roll back without it only when a memory_pending_claims table resolves on the connection's search_path; without that table they neither check nor need the index. A cascading delete of a node that does not exist still succeeds without it. Deleting a row's claim history is then an index lookup, not a scan of the claim ledger. These operations disable sequential scans, hash joins and merge joins for their own transaction. Those settings steer the planner; they do not forbid a plan. On PostgreSQL 16, under forced custom and forced generic plans, with 50,000 rows of claim and node history in the same owner scope, the tested statements read no other row's history, except that the per-row claim update may read the same row's earlier claims. Other PostgreSQL versions, other plan settings, larger or differently shaped histories, and edge-heavy scopes are untested. See the memory persistence guide for the PostgreSQL backend setup.
  • Native PostgreSQL recall (retrieve) now refuses with MemoryContractError when the scopes it may read hold more than 4,096 memories, instead of scoring them all until the 1 s deadline. It never returns a truncated page. It also refuses until an operator installs the index idx_nodes_scope_path_c with scripts/migrations/core204_c_scope_index.sql. The 4,096 count includes rows recall never returns, such as pending, retracted, expired and other sessions' rows. Legacy v0 scopes are unchanged. See Install the native scope index in the memory persistence guide for the PostgreSQL backend setup.
  • Optional: once an operator installs the index idx_nodes_candidate_eligible_c with scripts/migrations/core199_candidate_eligible_index.sql, native PostgreSQL recall counts only the memories it could return toward that 4,096 limit. Pending, retracted and unapproved rows no longer count, and neither do expired rows or other sessions' rows. Recall with an empty cue returns its ranked page even when more than 4,096 memories qualify, except in scopes deeper than 23 levels or with a large limit across many scope levels and layers, where it refuses rather than return a partial page. Recall with a cue still refuses above 4,096 qualifying memories. Memories whose tenant, scope path, layer and id together exceed 2,560 bytes are kept in the companion index idx_nodes_candidate_wide that the same script installs; they are still written and recalled, and each counts toward the limit. Without both indexes, or on a server other than PostgreSQL 16 with 8 KiB pages, recall counts every visible row toward the 4,096 limit, as in the entry above. See Count only returnable memories.
  • Native PostgreSQL subtree erasure now refuses, and rolls back, when the subtree holds more than 16,384 pending-claim records. Rewrites accumulate these records, so a subtree with few live memories can still exceed the limit. Before, erasure deleted every record while it held locks that stall writers for all tenants, and above roughly a million records it refused at the 1 s deadline. Those very large subtrees now refuse in milliseconds, and a subtree at the limit erases in well under the deadline under any plan cache mode. Subtrees between 16,385 and roughly a million records used to erase and now refuse. There is no admitted erasure path for them yet. See the memory persistence guide for the PostgreSQL backend setup.
  • Native PostgreSQL subtree erasure and the native pending lifecycle now refuse to run under any isolation level other than READ COMMITTED, including inside a caller's REPEATABLE READ or SERIALIZABLE transaction. Under those levels erasure could report a subtree forgotten while a memory written during the erasure was still stored. The refusal leaves the caller's transaction usable. A missing range index error now names the index. See the memory persistence guide for the PostgreSQL backend setup.

This is a partial CORE-199 change. It does not complete CORE-199 or retire the engine.

Added

  • symfonic.migration.agent_from_config(config, provider, ...) builds a symfonic.Agent from an existing FrameworkConfig. It reads only synthesize_on_empty_final, agent.model and agent.recursion_limit. The last two are translated only when they differ from stock; a stock config keeps the Agent's own defaults, so it asks your provider for its declared model (gpt-4o on OpenAIProvider) and gets the native 10 rounds, not ModelConfig() and the legacy 24. Every agent.model field may differ from ModelConfig() except context_window, which only the legacy compaction node reads. Only the agent.model fields you changed are applied, on top of your provider's own default, so changing just temperature still asks OpenAIProvider for gpt-4o; set model_name to choose another model. The Agent itself uses only structured_output_repair_attempts; it passes the other changed fields to your provider unchanged, and which of them take effect depends on the provider. A recursion_limit below 3 is refused. Every other setting left at its stock value keeps the native default. Two changes are also accepted: False for auto_hydrate, auto_consolidate or lazy_tooling when you pass no capabilities of your own, and a retired setting at a value its retirement row calls inactive. Any other setting you changed raises UnsupportedFrameworkSettingError before anything is called, listing every such setting. The result can be returned from an AgentHost composer. See moving to 11.0.
  • Generated projects with Docker Compose can opt in to separate PostgreSQL logins: a migrator that owns the schema and a runtime that can only read and write rows. The opt-in also requires your own database superuser password and keeps PostgreSQL on localhost. The default is unchanged. See split PostgreSQL roles.

Fixed

  • An Agent with instructions still sends only those instructions as its system prompt, but it now emits SystemMessageDroppedWarning when history has a system message after the first position, which used to be dropped silently. The warning gives the count and positions, never the content, and points at the line that called run; a streamed turn's warning may point at the code that drives the event loop instead. Python's warning filters decide how often it is shown. With the currently supported LangChain stack, each affected request can show its own warning, so filter on the SystemMessageDroppedWarning category if you want it quiet; if you turn warnings into errors it is raised as an exception. Any system message as the first history item is still dropped without a warning, because that is where result.messages carries the instructions that were used and they can change from turn to turn. A SymfonicAgent turn served by the kernel route with a compiled system prompt warns the same way; a turn on the legacy route is unchanged and does not warn. See multi-turn, without a session.

[11.2.6] - 2026-10-10

This private-registry candidate lets Voice validate a receipt-backed Agent second turn with authenticated, nonempty history (CORE-381, CORE-332). Package preparation is not adopter validation.

Fixed

  • Text-only typed history and tool calls remain model-visible and bound to the receipt request digest; forged, oversized, or unrepresentable history fails closed before model effects.

This candidate does not establish Voice parity, production staging, default route cutover, fallback unreachability, or engine retirement. It includes no tag or public PyPI release.

[11.2.5] - 2026-10-10

This private-registry candidate lets Voice validate staged PostgreSQL receipts and extracted facts (CORE-372, CORE-380). Package preparation is not adopter validation.

Added

  • Exact raw PostgreSQL reads and caller operation receipts use kind-aware owner identity and durable claims. Selected FLUSH binds to the accepted WRITE and exact selected records.
  • Bound extraction on the staged PostgreSQL path validates facts before WRITE and binds their identities to the run and staged receipt.

Fixed

  • A writer login cannot directly publish staged rows through public FLUSH under the separate writer and publisher database role boundary.

Staged PostgreSQL APIs remain restricted to disposable test databases: plaintext staged payloads still lack encryption and erasure. This candidate does not establish Voice parity, production staging, default route cutover, fallback unreachability, or engine retirement. It includes no tag or public PyPI release.

[11.2.4] - 2026-10-10

This private-registry candidate lets Voice validate tools and delegated children in the original scoped receipt turn (CORE-379). Package preparation is not adopter validation.

Fixed

  • Factory-issued scoped receipts now admit one exact tool-only delegation capability and empty prompting. Successful completion seals the original turn output digest after ordinary tools and delegated children execute.
  • Arbitrary capabilities, delegation subclasses or duplicates, nonempty prompting, and unissued receipts remain refused. Cancellation, foreign scope, and mismatched claim ownership cannot produce an accepted READY receipt.

Staged PostgreSQL APIs remain restricted to disposable test databases: plaintext staged payloads still lack encryption and erasure. This candidate does not establish Voice parity, production staging, default route cutover, fallback unreachability, or engine retirement. It includes no tag or public PyPI release.

[11.2.3] - 2026-10-10

This private-registry candidate packages the staged PostgreSQL APIs for Voice validation. The staging capability factory remains restricted to disposable test databases: plaintext staged payloads still lack encryption and erasure.

Added

  • Opt-in publication requires committed local final receipts and database-attested provenance, then makes staged graph/vector rows visible with their aggregate receipt in one transaction.
  • Separate staged writer and publisher login identities enforce an operator-installed PostgreSQL boundary. Activation and status reads refuse when that boundary or the required identity is missing.

Fixed

  • A restored role boundary cannot make a forged activation valid.

This candidate does not establish Voice parity, production staging, default route cutover, fallback unreachability, or engine retirement. It includes no tag or public PyPI release.

[11.0.2] - 2026-10-03

This 11.0.2 source candidate adds the opt-in exact-owner QUICK window, dispatch-outcome telemetry, and scope-safety guards for native memory composition and the still-live compatibility facade. It does not alter the 9.12 compatibility pin or retire legacy fallback.

Added

  • quick_phases(exact_session_owner=True) restricts the recent semantic window to the terminal session owner before applying any result limit. Deployments need a graph backend that supports exact subtree queries. Generic QUICK retains its ancestor-visible default.
  • An optional dispatch_outcome_sink observes real compatibility-agent dispatches with bounded route/outcome metadata. Hosts must explicitly install and monitor the sink; the journal alone is not a fleet denominator or retirement proof.

Fixed

  • Memory-capable compatibility turns with a hierarchical TenantScope that loses a kind or ID in Kernel conversion use the existing counted legacy fallback with the original scope on run, stream, and stream_typed. Representable scopes still use Kernel; the facade remains live.
  • Native Agent memory composition refuses paths over three levels, empty or disagreeing path/segment levels, and converters that drop a representable level before store access. One- to three-level positional keys remain; same-ID, different-kind scopes at three levels can still collide. CORE-171's kind-aware migration remains open.
  • Exact-owner QUICK refuses optional profile, episodic, and merge phases until their separate reads have the same ownership guarantee; malformed subtree results also fail closed. Graph subtree reads filter retracted rows correctly.

[11.0.1] - 2026-10-03

This update advances the 11.0 package and documentation metadata. It does not change the 9.12 compatibility pin for retired per-call arguments.

Added

  • Native Agent and generated chats can use opt-in, session-bound persistent conversation history. Native memory administration and procedure review routes have grown; see the memory persistence guide and production adopter patterns.
  • Deployment evidence reports expose more cases where fallback or native capture is incomplete. Operators should use the fallback deployment evidence guide when assessing their own deployment. A report is evidence for a decision, not a declaration that legacy fallback is unreachable.

Changed

  • Scoped graph operations and generated-host startup checks more strictly protect memory ownership, including PostgreSQL identities that compare differently under a database collation. Review the production guide before a scoped-edge cutover.
  • Procedural review lifecycle is fail-closed. Native Phase 12 output stays draft by default. Optional ProcedureReviewService automation is explicit, audited and seals its terminal decision so later promotion/dedup passes do not re-draft it. Integrations that wrote procedural lifecycle fields straight to the graph must use the procedural review/edit operations instead. Existing unsealed terminal rows remain historical evidence; newly written compatibility auto-approved rows are explicitly marked so duplicate legacy replays can be compacted without treating old exports as human approval. Generic staged writes cannot replace a reviewed procedure by reusing its ID; see the consolidation and review guide for the draft and review flow.

Fixed

  • Streaming routes preserve provider failures and enforce runtime deadlines. Native DEEP status is available when QUICK is disabled, and concurrent DEEP leases cannot overlap across scopes.

[11.0.0] - 2026-09-10

This is a published release and package metadata declares 11.0.0. It is a major version because three per-call arguments are retired and stream_typed is served by the kernel projection; both are listed below.

The compatibility facade and legacy implementations remain in the source. What changes is which body serves a turn on this line, not what the source contains. Adopters who depend on a retired argument stay on 9.12, the newest release that still honours it; the retirement is not backported.

Physical removal of the legacy implementations is a separate, explicitly authorized operation behind its own gate, and is not part of this release.

Added

  • Executable adopter migration recipes. The 11.0 migration guide now documents and links runnable scope-key round-trip, transcript ConversationSource, and legacy-fabrication-to-governance adapter recipes. The fabrication recipe records the public verdict from the legacy detector's actual finding confidence; it does not claim equivalence for legacy inputs absent from the public detector contract.

  • Eval CI gates in the source repository. PRs run deterministic scaffold evals; nightly/manual jobs run the real-model catalog. Public publication requires live validation of the same wheel being uploaded. Maintainers must configure dedicated test services and require the offline check in branch protection; adopter projects do not automatically inherit these workflows.

  • Public kernel composition and a kernel-native scaffold. Compose memory, prompting, tools, delegation, human interaction, governance, extensions and telemetry through public entry points. Generated applications use that composition for their agent, UI/API and background memory work.

  • Durable memory and semantic retrieval. Automatic post-turn extraction can persist facts for recall across conversations in the same scope. GraphBackedHms supports a vector backend paired with an embedder; the scaffold explicitly reports graph-only operation when embeddings are not configured. Operational degradation is reported rather than presented as an empty successful extraction or retrieval.
  • QUICK, NIGHTLY and DEEP consolidation factories. QUICK supports turn cadence; the scaffold's scheduled and manual Deep Sleep paths use the capability runtime. Scope leases, commit-time fencing and a single declared transaction domain protect consolidation writes. Procedural memories have separate draft, human review, approval and tool-precondition enforcement steps; a discovered procedure is not automatically authorized.
  • Conversation and memory observability. Correlated model, tool and stage events, durable counters and memory-lifecycle details are available to the scaffold's admin views. The optional engineering observability stack adds Grafana, Prometheus and Tempo. Input/output content capture is opt-in and subject to redaction and access controls; metadata tracing does not imply that prompt or memory contents were recorded.
  • Reusable agent regression evals. symfonic.evals and generated scaffold suites provide deterministic fast/integration profiles and configured live runs, JSON/JUnit reports, capability applicability, and the Glass Harbor book fixture. Journeys cover recall, grounding, tools, delegation, isolation, injection, consolidation and observability. Restart, concurrency and bounded memory-work checks complement response assertions. Non-applicable scenarios are reported explicitly, not counted as passes.

Changed

  • Kernel-first onboarding and versioned documentation. The site separates historical 9.x documentation, built from v9.12.0, from 11.0, which it now serves at the root. Intent-tooling guidance starts with public kernel composition and labels the FrameworkConfig path as compatibility.
  • Examples use declared public surfaces. Migrated examples exercise the capabilities they teach; retained compatibility, historical-comparison and archive examples are classified explicitly. This is not physical legacy removal.

Fixed

  • A contributed extension policy is now consulted before the call it governs — check yours before upgrading. The decision machinery worked, but nothing on a turn asked it, so a policy could compose cleanly, report success, and never run. It now runs, and a deny prevents the tool call. If you contribute policies, a deployment that appeared to work may have been working because the policy was never consulted; verify what yours decides before you upgrade. An abstention — which is what a policy that raises becomes — denies, matching the behaviour that already existed elsewhere.
  • A pre-model stage is dispatched. A capability declaring one compiled into the plan, took its place in the compiled order, and ran in no pass. If you declared such a stage, it now executes, once per model round.
  • Credential hygiene distinguishes "nothing to examine" from "examined and clean". At ingress on a kernel turn it has no subject — the request carries no key/value bag and the tool calls do not exist yet — and it reported that as a clean pass. It now reports no-subject. This changes the evidence a turn records, not what is scrubbed. Ingress free text is still not scrubbed.
  • Python 3.11 can import the kernel's immutable mapping defaults.
  • The Anthropic extra now requires SDK <1.0, preserving the documented httpx.AsyncClient injection contract until an explicit SDK 1.x migration.
  • AWS scaffold imports and migrations no longer initialize memory-model SDK clients. Credential and region resolution occurs when a model is used; this does not replace missing AWS configuration with a mock response.

  • Governance refusals take precedence over repairs. Argument amendments reach the dispatched call, and tool preconditions inspect the final arguments. Credential hygiene scrubs matching credential keys before dispatch; it is not a general detector of secrets embedded in arbitrary text. Decisions retain declared rule attribution without exposing state values.

  • Extension tools bind callable implementations and argument schemas; delegation delivers child responses with scope and run correlation. Forced tool choice respects provider support instead of failing through an unadapted binding call.
  • Memory metadata round-trips preserve producer data while protecting reserved authorization fields. Retrieval respects scope visibility and retractions; consolidation preserves phase order and rolls back failed publication.
  • Documentation API discovery traverses the packaged eval fixtures, and the version selector remains usable on generated 404 pages.

Upgrade and validation notes

Capabilities must be composed explicitly; a FrameworkConfig flag alone does not configure the corresponding kernel capability. Pause-token compatibility depends on the redemption path. Bounded consolidation work does not promise bounded total database size or constant-cost vector search. Validate a newly generated project against your own providers and storage before rollout.

Stored scope keys do not need converting. Rows persisted under the legacy delimited scope_path are read by the capability path as written: vector recall tries both spellings before skipping a row, graph rows read the stored path back into a scope, and a pre-v8 row carrying no scope_path at all reads as tenant-global. scope_from_legacy_path is exported from symfonic.capabilities.memory for code that needs the conversion explicitly, but recall does not require you to call it. The contract is asserted by tests/capabilities/memory/test_legacy_persistence_compat.py, including the direction where the capability path reads a node it never wrote.

See the transition guide, migration guidance and agent regression evals.

[9.12.0] - 2026-08-08

Added

  • Prompt blocks — a pinned standing-context lane inside the cached prefix. FrameworkConfig.prompt_blocks declares named blocks (BOUNDARIES, IDENTITY, RULES, ENVIRONMENT, USER_PROFILE, …), each served by one source and rendered into the L0 + L1 cached region rather than the uncached MEMORY_CONTEXT placeholder. Resolution is one deterministic read per block — no query, no similarity score, no top-K truncation — so a pinned fact is never dropped on the turns that do not happen to mention it. The rendered bytes are memoised per scope under the blocks' own durable revisions, so an unchanged turn returns the same string and re-bills nothing. Adds no new cache breakpoints: blocks are text inside the existing L1 section.
  • Four tiers with a trust boundary. platform / operating are authored and render verbatim; profile / session are learned and render inside <untrusted-data> delimiters with per-fact provenance. The precedence rule (PLATFORM > OPERATING > PROFILE > SESSION) is stated once in the region header instead of being left to the model to infer from concatenation order.
  • Block source adapters: StaticBlockSource, FileBlockSource, ComputedBlockSource, the "memory" lane, and DatabaseBlockSource (rides the existing [postgres] extra). Capabilities — offline_safe, scope_aware, history, writability — are structural: validation asks isinstance(source, HistoryCapableBlockSource) rather than reading a self-reported flag, so an adapter for a store with no versioning cannot claim a capability it does not have.
  • examples/prompt_blocks — a runnable walkthrough of the taxonomy, the capability matrix, the rejected pairings, the rendered region and the cache accounting. A config literal, a temp file, a pure callable and an in-memory graph: no model, no network, no database. Run it with python -m examples.prompt_blocks.
  • docs/concepts/prompt-blocks.md — the concept guide: which block to use for what, who is allowed to write each one, the adapter capability matrix, the adapters deliberately not shipped and why (Consul KV and Vault KV v1 are read-only-only, not missing), and the cache rationale.

  • PromptBlockSpec.render_when — a host-supplied predicate, checked before the source is read, so a gated-off block costs no I/O. This is what lets an ONBOARDING block delete itself once its job is done: a permanent onboarding block is waste on turn 500 and keeps steering the agent. A predicate that raises fails the turn (BlockGateError) rather than being absorbed by on_source_failure, because a gate's failure leaves no trace in the rendered prompt at all.

  • EMBEDDING_BASE_URL / EMBEDDING_API_KEY — point embeddings at a self-hosted server without dragging the chat model with them. The OpenAI SDK reads OPENAI_BASE_URL for every client, so the two could not diverge before. No API key is required when EMBEDDING_BASE_URL is set: naming your own endpoint says api.openai.com is not the target, and self-hosted servers commonly take no auth.
  • examples/block_boundaries — where a block stops: what a delegated child inherits, what each on_source_failure policy does, and how render_when removes a block. Copyable with symfonic examples add, and runs with no model, network or database.
  • Guide: Standing-Context Blocks — choosing a source, the memory-lane prerequisite that fails at construction, failure policy, cache cost, and what a delegated child inherits.
  • Calling run() without a scope is supported, and it partitions rather than accepting or refusing wholesale. scope defaults to None on run() / stream(), so a CLI or a single-tenant deployment reaches this on the ordinary call shape. A block needs a scope only if it is not deployment-scoped, or its source declares scope_aware, or it carries a render_when gate — the gate is called with the scope, so without one there is no question to ask it. Everything else is deployment-global and renders normally: a StaticBlockSource ignores the scope by declaration, so there is nothing for a missing one to change. Blocks that genuinely are scope-keyed cannot be resolved, and each one's own on_source_failure decides what that means — fail_closed raises SecurityScopeError naming the blocks and the remedy, omit and last_known_good degrade as declared. The alternatives were both wrong in the same way: refusing every fail_closed block deletes an authored BOUNDARIES block from every single-tenant deployment, and omitting silently deletes it too, just without saying so.

Fixed

  • ask_user structured elicitation now works end to end. It was unreachable, and each defect hid the next: the tool was bound to the model but never entered tool_manifest, so the prompt's "use ONLY these exact names" list did not authorise it and no model called it (verified against five models); stream_typed recognised only the older interrupt shape, so the graph paused and the caller was told nothing; resume() lost its dependency container and had no interrupt interception, so a second question inside a resumed run parked silently; and a model emitting a question with fewer than two options killed the run outright.
  • An abandoned ask_user question no longer breaks the conversation. A user who never answers used to leave an unanswered tool call that every later turn replayed — rejected by providers that validate tool-call pairing — plus stale state that re-asked the dead question forever. Both are now repaired on the next turn without mutating the checkpoint, so a late answer still resumes.
  • A stale pause no longer strands the router. ask_user_pause_ttl_seconds now governs router staleness as well as pause-token validity, so a caller threading a stable run_id through run() for tracing still recovers. _interrupt_pending carries the same guard for experimental_interrupt users. The preset receives AgentConfig, not FrameworkConfig, so the new symfonic.core.config.AgentConfig.ask_user_pause_ttl_seconds field had to exist for the router guard to be reachable at all.
  • resume_interrupt() never resumed. It passed a state key nothing read, so the parked node's interrupt() re-fired instead of returning and the run could not advance. It now uses Command(resume=...), as resume() does.
  • A render_when predicate that raises now fails the turn at the engine. The gate refused correctly, and a blanket except Exception one layer up turned the refusal into "no blocks" — a declared policy discarded by the boundary meant to honour it.

Upgrade note. Threads already carrying an abandoned pause from before 9.12.0 have no minted_at stamp, so the age check cannot help them; the repair applies to new abandonment. The unanswered-tool-call stub text also changed, which re-bills the cached prefix once for any thread carrying an orphan.

Changed

  • Memory-hydration budgeting no longer calls tiktoken. It preferred an exact token count when tiktoken happened to be importable — which was never a decision: tiktoken is not a declared dependency, it arrives transitively with langchain-openai, so [openai] counted exactly while [anthropic] used a character estimate and the same configuration produced two different hydration caps. It also downloads its encoding on a cold cache, with no timeout, while holding a process-global lock, on a request path. Hydration now uses a character estimate everywhere (len // 3), so the cap is deterministic across installs and nothing on this path can reach the network. Expect a slightly different truncation point than 9.11 if you were on an OpenAI-family extra; this is a sanity ceiling on context size, not token accounting.
  • An expired pause token now raises code="gone" instead of "unauthorized". A timeout and a tampered token were indistinguishable; one deserves "the question expired, ask again" and the other a security alert. Forged tokens keep "unauthorized". Breaking if you match on that code.
  • DomainTemplate.onboarding_checklist documentation corrected. It reaches the model only inside the memory-extraction directive, as a recording instruction. Nothing tells the agent to ask for those fields, and core has no completion signal that would tell it to stop — if you want active onboarding, author that instruction yourself and give it a stop condition.

Security

  • No block grants a destructive verb, at any tier. AgentPermission is exactly read / append / replace / rewrite; clear and delete do not exist. The check runs at import against the literal itself, so widening it later fails loudly rather than quietly granting a delete. Removal happens only through the memory layer's soft-retract path.
  • One writer per block, and it is never the agent. No block-edit tool is registered on any construction path in this release, and core writes to no block source on any prompt or agent path. The only write verb shipped — WritableBlockSource.append_revision — is an operator-facing API the host application calls from its own admin surface; that system owns its authorization and its audit trail. The verb is append only: a restore is expressed as appending the restored content, so history stays strictly additive.
  • The child lockdown is live before the tools exist. symfonic.agent.subagents.lockdown strips prompt_block_self_edit from every delegated child config (including an explicitly-supplied SubAgentSpec.config, which is sanitised rather than trusted) and rejects a pre-built child carrying a block-edit tool. The guard matches the whole memory_block_* namespace, so a verb added later is caught rather than admitted.
  • Misconfigurations are construction-time errors, not runtime checks. A writable platform block, a per-tenant block on a deployment-global source, an operator_editable block on a source that cannot show its history, and an authored tier backed by the memory lane are all unrepresentable in a valid configuration — the process does not start.

Unchanged

  • prompt_blocks defaults to (), and that default is byte-identical. With no block declared, nothing is resolved, no fragment is spliced into any prompt region, and every prompt path renders exactly as it did before this release.

[9.11.0] - 2026-08-04

Added

  • promote_profile_corrections — user profile corrections are now written onto the tenant's own SOUL: node in the graph, scope-keyed, instead of into a shared config dict. Consolidation gains a profile_fields: frozenset[str] argument: the domain's soul_schema is read to learn which field names make up a profile, and is never written to. A domain declaring timezone now gets timezone corrections promoted — the previous implementation only ever handled a hardcoded name / role / personality / language.
  • ConsolidationReport.profile_updates, mirroring the existing soul_updates (which is unchanged, so no consumer's key set breaks).
  • examples/memory_lifecycle — a runnable walkthrough of the memory lifecycle: how an operational fact ages out, why identity never does, that retirement is reversible, and how a user correction reaches the profile. In-memory only; no model, no network, no database. Run it with python -m examples.memory_lifecycle.

Fixed

  • Profile corrections no longer corrupt the extraction schema. The previous apply_soul_corrections wrote instance values into DomainTemplate.soul_schema — a dict[str, str] of field names to their expected types, which is rendered to the model as Schema: {...} to shape extraction. {"name": "str"} became {"name": "Amiel"}, degrading every subsequent extraction for that domain.
  • A cross-tenant leak in the generated worker. The scaffold template built one soul_schema dict above its per-tenant consolidation loop and passed the same object to every tenant; in-place mutation meant one tenant's profile values accumulated into the next tenant's consolidation. Every project generated by symfonic init carried this. The new signature takes an immutable frozenset, so the leak is gone by construction rather than by remembering to copy.

Deprecated

  • SleepConsolidator.run(soul_schema=...), and the same argument on quick_nap() / nightly_nap(). Still accepted, and still functional: when supplied without profile_fields, the field set is derived from soul_schema.keys() and a DeprecationWarning is emitted. The dict is now read-only on every path — nothing mutates a caller's dictionary any more. Pass profile_fields=frozenset(...) instead.

[9.10.0] - 2026-08-04

Added

  • Semantic memory now expires. A new consolidation phase (9.2) soft-retracts SEMANTIC nodes whose durability marker says expired, whose transient TTL has elapsed, or whose valid_until timestamp has passed. Until now the SEMANTIC layer had no expiry path at all: cleanup_working_ttl only covers WORKING, decay_importance lowers importance but never removes anything, and retract_node fires only on an explicit user correction. A fact that quietly stopped being true — "the Slack integration is broken" — stayed in the agent's context forever. Expiry is entirely time-based; no model is ever asked whether a memory is stale, and the terminal action is a soft retraction that flows through the existing 7-day grace window, never a delete.
  • INCIDENT: label prefix. Nodes labelled INCIDENT: are treated as transient even without an explicit durability, so operational facts age out on their own. This is the first prefix convention that makes a node expire faster rather than slower — the deliberate opposite of SOUL: / AGENT_IDENTITY:, which are exempt from decay. A future valid_until extends an incident's life; nothing makes one permanent.
  • ConsolidationReport.nodes_expired reports how many nodes phase 9.2 retracted.

Fixed

  • A sub-agent no longer overwrites the tenant's agent identity. A delegated child inherits its parent's tenant scope but is built with a domain named after its own spec, so a child that ran before its parent for a fresh tenant would seed AGENT_IDENTITY: <child-name> onto the shared identity node — and because the seeder early-returns once any identity node exists, the parent then silently and permanently inherited its child's identity. Only a top-level run may now seed.
  • SOUL: nodes are excluded from LLM-judged semantic merge. They were eligible, so the judge could absorb SOUL: name into SOUL: role and stamp a supersede-retraction on the loser, corrupting the user profile. AGENT_IDENTITY was already excluded for exactly this reason.

Backward compatibility

Nothing changes at default settings. Phase 9.2 only acts on nodes that carry an explicit durability marker, a valid_until timestamp, or an INCIDENT: label; a node with none of these is untouched. Existing SEMANTIC data is unaffected — promotion into SEMANTIC already requires durability == "durable", so no stored fact starts expiring on upgrade.

[9.9.0] - 2026-07-19

Added

  • Kimi OAuth auto-refresh. KimiOAuthProvider.from_kimi_cli() reads the kimi-cli credential file and auto-refreshes the short-lived (~15 min) token on every request — local dev sessions no longer 401 mid-run or need a manual kimi refresh. The rotated refresh token is written back safely (file lock + atomic write) so your kimi-cli login stays valid; tokens are never logged. Explicit KIMI_OAUTH_ACCESS_TOKEN opts out (unchanged).

Changed

  • Clear, actionable auth-failure messages for all OAuth providers. An expired/revoked subscription token now names the exact remediation (kimi-cli login / codex login / claude login) via a clear error or a log-only WARNING on 401/403, instead of an opaque SDK error.

[9.8.0] - 2026-07-19

Subscription OAuth providers for local dev/test. Three providers now let a single developer drive the full pipeline against a real LLM using the OAuth token their CLI already stored — no metered API key. They are not a production path: none bill per tenant or refresh tokens, each replays a personal subscription token (which may be throttled/disallowed under the vendor's ToS), and each refuses to construct in a production-like environment. See the Subscription OAuth providers guide.

Added

  • KimiOAuthProvider (symfonic.core.kimi_oauth_provider) — dev/test-only provider that calls the Kimi/Moonshot OpenAI-compatible backend with a kimi.com subscription token instead of a metered MOONSHOT_API_KEY. Env-vars-only (KIMI_OAUTH_ACCESS_TOKEN, KIMI_OAUTH_BASE_URL, KIMI_OAUTH_PLATFORM); production guard identical to CodexOAuthProvider. Defaults to the kimi.com subscription/coding endpoint (https://api.kimi.com/coding/v1, model kimi-for-coding) — the only endpoint a subscription token authenticates against — so subscription tokens work without an override.
  • Live (live_llm) tests for all three OAuth providers — each sends a real SystemMessage + HumanMessage round-trip and is deselected by default.

Changed

  • AnthropicOAuthProvider is no longer deprecated. It is now a supported dev/test-only provider (same posture as CodexOAuthProvider): construction no longer emits a DeprecationWarning — instead it logs a one-time ToS caveat. It still auto-loads the Claude Code token from the Keychain / ~/.claude/.credentials.json and is never for production.

Fixed

  • CodexOAuthProvider now works through the normal agent path. The Codex Responses backend rejects role: "system" (400 System messages are not allowed), which crashed every SymfonicAgent run on the first call. The provider now rewrites outgoing system messages to the developer role on the Responses-API path. (Verified live against the Codex backend.)
  • symfonic init --llm-provider openrouter now points adopters at OPENROUTER_API_KEY instead of a generic LLM_API_KEY hint.
  • KimiProvider observability — Kimi spans now report gen_ai.system="openai" (was "unknown"), consistent with its DeepSeek sibling.

[9.3.0] - 2026-07-13

Lazy-tooling skill-resolution contract (issue #36). No API changes.

Changed

  • lazy_tooling validates procedural skills against your registered tools. A skill's tool identifier is its action_tool metadata (else content) and must be the exact name of a registered tool. A name that matches nothing is now dropped with a LazySkillResolutionWarning instead of leaking into the system prompt as a phantom tool. Correctly-named skills are unaffected; put descriptive text in steps / context, not in the identifier field.

[9.2.1] - 2026-07-13

Correctness patch from an external review of the 9.1/9.2 work. No API changes.

Fixed

  • Agent shutdown no longer blocks on unrelated agents. Background consolidation is now tracked and flushed per agent, so exiting one agent's async with never waits on another agent's pending work.
  • Provider-family extraction respects routing. The family-tuned extraction template is now selected from the resolved model config, so a router serving a different vendor than its default gets the right template.
  • Config facades validate overrides. FrameworkConfig.child(...) and AgentBuilder now re-validate the assembled config, enforcing field constraints and warning on unknown fields instead of silently keeping them.
  • Declarative sub-agent descriptions preserved. A SubAgentSpec with only description set now gives the child a matching domain description (was empty).

[9.2.0] - 2026-07-12

Facade minor: a fluent builder over the 9.1 construction surface and provider-family-aware memory extraction. Fully additive — no breaking changes.

Added

  • AgentBuilder — a fluent way to assemble an agent. Chain .provider().model().domain().tools().enable_hms().memory().sub_agent().build() instead of hand-wiring FrameworkConfig + SymfonicAgent. It's a thin facade over the same primitives (FrameworkConfig, SubAgentSpec, HMSFactory) — no new behaviour — and .memory(embeddings=…) builds an in-memory HMS for you.

Changed

  • HMS memory extraction adapts to the provider family. OpenAI-family and Google agents now render a more explicit extraction directive (a strict <GRAPH_OPERATIONS> JSON protocol) instead of the Anthropic-delimiter default, reducing silent memory loss on instruction-following models. Anthropic and unrecognised/custom providers are unchanged (wire-neutral). An explicit extraction_template_path still overrides the family default.

[9.1.0] - 2026-07-12

Construction-DX minor for sub-agent ergonomics and non-Claude memory reliability. Fully additive — no breaking changes.

Added

  • Declarative sub-agents — SubAgentSpec. Describe a child agent and let the parent build it: SymfonicAgent(sub_agents=[SubAgentSpec(name="researcher", description="deep research", tools=[...], temperature=0.2)]). The parent constructs the isolated child at wiring time, inheriting the parent's ModelProvider and every behaviour flag, but with its own fresh domain so its tool manifest auto-derives from its own tools. The pre-built SubAgent(agent=...) form remains as the escape hatch; mix both freely.
  • FrameworkConfig.child(parent, name=, description=, model_name=, temperature=, max_tokens=, **overrides) — derive a sub-agent config that inherits every parent flag and overrides only what you name. Stops per-child config drift.
  • Provider inheritance. A SubAgentSpec inherits the parent's provider by default (providers are stateless, so one instance is safe to share); pass provider= to override per child.
  • AgentResponse.delegated_to is now populated. The field (added empty in 9.0.1) is wired: a parent run records the sub-agent(s) it delegated to via run_agent and stamps them, in order, onto its final response.
  • FrameworkConfig.extraction_template_path — point HMS memory extraction at a custom directive template. The bundled template is Anthropic-delimiter oriented; non-Claude adopters (Qwen/DeepSeek/vLLM) can supply a JSON-only directive to stop silent memory loss when the model ignores the delimiter.

Changed

  • async with SymfonicAgent(...) now flushes background memory work. __aexit__ awaits flush_background_tasks() before releasing resources, so the idiomatic context-manager exit no longer silently drops fire-and-forget consolidation. aclose() stays a pure resource-release for the hot-reload path. A best-effort atexit guard emits a RuntimeWarning if a process exits with consolidation still pending — turning silent memory loss into a visible signal.

[9.0.1] - 2026-07-12

Agent-experience patch from autonomous-adopter feedback. No breaking changes.

Added

  • symfonic guide and a wheel-resident AGENTS.md — an offline quick reference (decision tree, canonical imports, a real quickstart, footguns) discoverable from an installed wheel, no repo needed. Plus llms.txt for online agents.
  • AgentResponse.delegated_to — the sub-agent(s) a parent delegated to (tuple[str, ...], empty = answered directly).

Changed

  • tool_manifest auto-derives silently from your registered tools (was a UserWarning); this closes the "tool registered but the LLM can't see it" footgun. Tools marked @symfonic_tool(visibility="hidden") are excluded; an explicit manifest still wins.
  • symfonic.core now points to symfonic.agent in its docstring, and the LazyToolingWarning carries an actionable Fix: line.
  • Sharing one ModelProvider across a parent and its sub-agents is safe and documented (providers hold no per-call state).

Fixed

  • Non-Claude memory-label corruption — labels are whitespace-collapsed and length-capped so a model that ignores the extraction format can't persist its whole response as a node label.

[9.0.0] - 2026-07-10

Changed (breaking)

  • Migrated to the langgraph 1.x / langchain-core 1.x line. Core now requires langgraph>=1.0,<2.0 and langchain-core>=1.0,<2.0; the 0.x line is no longer supported. Provider extras admit the langchain-* 1.x releases (anthropic, openai, aws, google, ollama, chroma). A plain pip install -U symfonic-core pulls the aligned 1.x set. Step-by-step upgrade instructions: Migrate 8.x to 9.x.

Fixed

  • The durable Mongo checkpointer is now installable. It shipped in 8.12.0 but pip install "symfonic-core[mongodb]" could not resolve — the Mongo adapter requires langgraph-checkpoint>=3 (i.e. langgraph>=1.0), which the old langgraph<1.0 core pin forbade. On the 1.x line the whole checkpoint cluster (postgres/sqlite/mongo) resolves consistently.
  • Tool errors no longer crash a run on langgraph 1.x. langgraph 1.x changed the tool-node default to re-raise non-validation tool exceptions; symfonic now restores the prior behavior where a raised tool error is fed back to the model as a recoverable message instead of aborting the graph.
  • Anthropic http_client transport injection works across the langchain-anthropic 1.x line (the seam's version gate now spans the 1.x releases, still guarded by its structural probe).

[8.12.0] - 2026-07-09

Added

  • Durable checkpointer for MongoDB. A Mongo graph backend now wires a durable MongoDBSaver (needs pip install "symfonic-core[mongodb]") instead of silently falling through to the ephemeral in-process MemorySaver — so resumable interactive sessions and durable transcripts survive a restart on Mongo deployments. ask_user pause tokens are made durable on Mongo too via a MongoPauseTokenStore. Selected automatically from the graph backend, exactly like the Postgres path; no config change required. See docs/concepts/conversation-persistence.md.
  • Conversation managers. Named, swappable strategies for keeping history in the context window — SummarizingConversationManager (default: summarize overflow), SlidingWindowConversationManager(window_size=N) (keep the last N messages, no LLM summary), and NullConversationManager (unbounded). Pass one as SymfonicAgent(conversation_manager=…). Thin wrappers over the existing compaction + windowing engine — orthogonal to HMS memory. See docs/concepts/conversation-managers.md.
  • symfonic examples CLI. Copy a curated, runnable example straight into your project without cloning the repo — symfonic examples list shows what's available and symfonic examples add <name> copies it into ./<name>/ and prints how to run it. The curated set (minimal_agent, basic_agent, tool_agent, skill_agent, sub_agents, memory_agent, e2e_basic_agent) is bundled in the wheel as data and runs on MockModelProvider — no API key, no extra dependencies. Requires the [cli] extra. Also includes real_agent, which runs against a real Anthropic model ([anthropic] extra + an API key); examples list / add state each example's requirements.
  • Full-code example walkthroughs. Each of the nine core learning-path examples now has a documentation page with the complete source and a step-by-step explanation, so the code is readable without a checkout.

Fixed

  • Durable checkpointer was never selected (Postgres or Mongo). The engine's graph-backend selectors read a non-existent .backend attribute (the store exposes _backend), so every deployment silently fell through to the ephemeral in-process MemorySaver regardless of a durable Postgres/Mongo graph backend — transcripts and resumable state did not survive a restart. Now reads the real backend. Surfaced by end-to-end testing the Mongo path.
  • Mongo checkpoints lost their channel values. MongoDBSaver serializes the whole checkpoint in one call; the safe serializer degraded the entire checkpoint to None when it contained a non-serializable runtime channel (deps), so get_transcript / restart-resume returned empty on Mongo. The serializer now sanitizes channel values per entry, keeping messages.
  • Examples install guidance. Corrected the docs claim that an editable/dev install "exposes" the examples tree — it's running from the repo root (which puts examples on sys.path) that makes python -m examples.<name> work; the extras are only needed for examples that import them.

[8.10.0] - 2026-07-08

Added

  • AWS Bedrock provider. Run any Bedrock model (Claude, Llama, Nova, Mistral, …) through the Converse API via AWSBedrockProvider (pip install symfonic-core[aws]). The wire family is auto-detected per model — Claude gets extended thinking, other vendors are treated wire-neutral. Region and credentials resolve through the standard boto3 chain; LLM_MODEL is a Bedrock model / inference-profile id. symfonic init --llm-provider aws scaffolds it end-to-end.
  • OpenRouter provider. Reach many vendors through one OpenAI-compatible gateway via OpenRouterProvider (pip install symfonic-core[openrouter]). LLM_MODEL is a fully-qualified id (anthropic/claude-sonnet-4.5); auth via OPENROUTER_API_KEY. symfonic init --llm-provider openrouter scaffolds it.

Fixed

  • Bedrock cost telemetry. Claude-on-Bedrock calls now report correct token cost (Anthropic list rates) instead of $0. Non-Anthropic Bedrock models need a pricing override.

[8.9.1] - 2026-07-08

Added

  • Sub-agent delegation. A parent agent can hand a self-contained task to a named child agent through the built-in run_agent / list_agents tools. Declare children with SymfonicAgent(sub_agents=[SubAgent(...)]); each child runs isolated, inherits the parent's tenant scope, and is bounded by a delegation-depth guard. See the Sub-agents guide.
  • Sampling controls on ModelConfig. New top_p (all providers) and top_k (Anthropic, Google, Ollama) knobs, alongside the existing temperature / max_tokens. See Model Tuning.
  • Scaffolded projects (symfonic init) now ship a working researcher sub-agent, a visible "delegating…" indicator in the chat UI, and tighter default reply lengths.

Fixed

  • The built-in mock chat model now streams incrementally, so streamed UIs and end-to-end tests observe real token deltas instead of an empty response.

[8.8.0] - 2026-07-07

Added

  • Pluggable metrics persistence. A storage-agnostic MetricsStore lets per-LLM-call usage and cost survive worker restarts, with Postgres and MongoDB reference backends. Fully additive — behaviour is unchanged when no store is configured.

[8.7.1] - 2026-07-03

Fixed

  • Consolidated hotfix across the agent engine, memory backends, prompt cache, pricing/observability, and metrics endpoints. Hardens state-key propagation, conversation isolation in the messages cache, precondition gating, and metrics-router authentication.

Packaging

  • The published source distribution is now curated: it contains the package, tests, license, README, and a hand-picked set of user-facing guides and examples. Internal tooling, local artifacts, and internal-only notes are no longer included.
  • Installation now defaults to public PyPI (pip install symfonic-core); no extra package index configuration is required.
  • Added an explicit MIT LICENSE file to the distribution.

[8.7.0] - 2026-07-01

Added

  • Opt-in rolling messages-region cache ladder (messages_cache_policy="rolling"). For long agentic tool-loops it converts the previously ~quadratic uncached token tail into a roughly linear one by holding prior cache markers as reads and adding one new marker per iteration. No default behavior change — the request wire is byte-identical under the default policy.

[8.6.0 – 8.6.9] - 2026-06

Changed

  • A series of reliability and correctness improvements across memory backends, prompt caching, usage/cost accounting, and the diagnostics CLI.

[8.0.0] - 2026-06-13

Added

  • Major release of the 8.x line: the agent orchestration layer, 5-layer Hierarchical Memory System (HMS), capability-based dependency injection, streaming event taxonomy, and the FastAPI bridge.

For upgrade guidance between major lines, see Migrate 8.x to 9.x and Migrate 6.x to 7.x.