Copyright 2026 Firefly Software Foundation. SPDX-License-Identifier: Apache-2.0.
This guide covers the 26.10.1 dependency refresh, PostgreSQL vector-store safety in 26.10.0, the Pydantic AI 2.x upgrade, and earlier API changes.
Resolve application dependencies normally after upgrading the release wheel.
The Pydantic AI minimum is now >=2.53.0,<3; the qualified lock uses 2.55.0 with
FastMCP 4.1.0, MCP 2.3.0, OpenAI 3.31.0 and Starlette 1.7.0. Existing provider
extras, native toolsets, model wrappers, output schemas and endpoint selectors
remain available. Do not bypass dependency resolution or retain incompatible
application overrides. The resolution record
lists all 23 Dependabot requests and the remaining optional-package advisories.
Applications using native Pydantic AI concurrency limiters must account for its
2.53 changes: a limiter cannot be shared between a calling agent and nested
model wrappers, acquisition is no longer reentrant, and custom limiters must
support release from another task. Pydantic AI 2.55 also accounts for usage from
rejected fallback attempts. Review cost reporting and custom wrappers when
upgrading; applications requiring that accounting correction need Pydantic AI
2.55 or later even though Agentic's minimum is 2.53. Firefly records aggregate
native fallback tokens, but does not attribute their dollar cost separately to
each attempted provider/model. Incremental streams now close unfinished native
text iterators before leaving the stream context, preventing deferred cleanup
errors after early termination. Firefly's sampling validation follows the new
thinking_enabled_by_default profile key; custom measured profiles should use
that key instead of openai_reasoning_enabled_by_default.
The [security] extra now requires cryptography 50 or later. Upstream stopped
publishing macOS Intel and Windows 32-bit wheels in version 49. A universal
Agentic wheel does not establish native dependency support on those platforms;
qualify a supported platform or an independently maintained source build.
The [binary] extra retains HEIF support and updates Pillow, pypdf, CairoSVG and
py7zr. The 7z adapter uses the current in-memory writer API and enforces member
and expanded-size limits before returning extracted artifacts. No database
migration, re-embedding or PostgreSQL upgrade is required.
The Mistral embedding adapter now supports the SDK 2 client module while retaining the 1.x import path. The qualified lock uses Mistral 2.10.1; local HTTP tests exercise construction, request serialization, response parsing and errors.
Previously, upserting an existing document ID into another namespace could
overwrite and move the original PostgreSQL row. Version 26.10.0 raises
VectorStoreError for that conflict and rolls back the whole batch, including
earlier inserts or updates. The check also applies to concurrent writes and
TenantScopedVectorStore.
The table still uses a global PRIMARY KEY (id). Same-namespace updates and
repeated IDs within one same-namespace batch keep their existing behavior. No
database migration, index rebuild, or re-embedding is required for this fix.
Before upgrading a shared-table deployment, check how the application assigns document IDs. IDs must be unique across namespaces. New documents in different scopes need distinct IDs; do not bulk-rename existing IDs without accounting for their references. If the application used cross-namespace upsert to move a document, replace that behavior with an explicitly authorized transfer workflow. Retrying the same conflicting batch will not succeed.
Install the current release, then verify same-namespace updates, rejection of a foreign-namespace ID, and whole-batch rollback in an isolated application test database. This fix does not restore content overwritten by earlier versions; any suspected historical loss requires review of application audit records or backups.
The wrapper requires scope arguments but does not authenticate them or install RLS policies. Keep authorization and transaction-local database scope setup in the application. See vector-store scoping and database authorization.
Pgvector/HNSW remains unchanged. PostgreSQL 18 and pg_vectorsearch are not requirements of this release; PRISM remains outside the supported backend.
The framework now requires pydantic-ai>=2.53.0,<3 and
genai-prices>=0.1.9,<0.2. Update application constraints that still require
Pydantic AI 1.x, then resolve dependencies and run your application tests. The
framework declares provider extras explicitly to preserve its existing provider
integrations under the new dependency layout.
The framework preserves the endpoint for existing configurations:
# Before and after the dependency upgrade: Chat Completions.
agent = FireflyAgent(name="assistant", model="openai:gpt-4o")
spec = ModelSpec(provider="openai", model="gpt-4o")
# Explicit equivalents for new/shared configuration.
agent = FireflyAgent(name="assistant", model="openai-chat:gpt-4o")
spec = ModelSpec(provider="openai-chat", model="gpt-4o")The same rule applies to azure: / provider="azure", with azure-chat: /
provider="azure-chat" as the explicit form. An existing preconfigured
OpenAIChatModel stays on Chat Completions; an OpenAIResponsesModel stays on
Responses. ModelSpec(model_class="OpenAIResponsesModel", provider="openai", ...)
remains valid.
Explicit API selection also applies to fallback models and evaluation judges. After a fallback run, Firefly restores the original model and identifier and resets the fallback chain, including when the run succeeds or fails.
If application code constructs a bare pydantic_ai.Agent, its openai: prefix
now selects Responses. Use openai-chat: there to retain the old endpoint, or
openai-responses: to adopt the new one deliberately. This also matters when
sharing a model string with an upstream library outside Firefly. See
Pydantic AI's OpenAI configuration.
from fireflyframework_agentic.agents import FireflyAgent
from fireflyframework_agentic.models import ModelOptions
# Reads OPENAI_API_KEY from the environment.
agent = FireflyAgent(
name="assistant",
model="openai-responses:gpt-6-luna",
model_options=ModelOptions(reasoning="low", store_responses=False),
)
first = await agent.run("The project name is Lantern. Acknowledge it.")
second = await agent.run("What is its name?", message_history=first.all_messages())Your tool definitions, output models, and run / run_sync / run_stream calls
keep the same interface. Pydantic AI handles each endpoint's request and response
shapes. Use provider="openai-responses" in ModelSpec, or azure-responses for
an Azure deployment that supports Responses. Custom OpenAI-compatible services
must implement the selected API; changing a prefix cannot add server support.
For GPT-6 model and endpoint restrictions, follow the compatibility table. In particular, migrating an Astra agent with tools requires Responses. Review reasoning effort and remove sampling/log-probability settings that the selected mode rejects, including any configured default temperature. See the official OpenAI migration guide.
- Prefer
ModelOptionsfor framework-owned settings, including reasoning and provider response storage. Passmodel_options=to an agent or individual run, oroptions=toModelSpec. Unsupported options raiseModelOptionsError. See the typed option reference. ModelSpec.settingspreserves native Pydantic AI provider settings such asopenai_reasoning_effortandopenai_store, alongside the translated console settings. Passfactory.settings_for(spec)toFireflyAgent(model_settings=...).- Pydantic AI 2.x model profiles are dictionaries. Replace custom code that uses
dataclasses.replace(profile, ...)or attribute access with dictionary merging and key access.ModelSpec.profile_overridesremains a mapping. message_historyand Firefly memory continue to carry conversation state. Optionalopenai_previous_response_id="auto"chains stored Responses using response IDs from that history. It requires storage; leave it unset withopenai_store=False. See the conversation-state example.- Completed buffered and incremental streams now persist their full messages to Firefly memory, including provider metadata and Responses reasoning state. Interrupted streams do not persist an incomplete conversation turn.
The offline HTTP compatibility suite checks the SDK/framework request boundary. It does not establish live model access. Use the credential-gated OpenAI suite for your selected deployment and model before rollout; commands and scope are in tests/README.md.
PostgreSQL and MongoDB clients now stay on a persistent event loop owned by their
store. This permits initialization and the synchronous memory facade to share the
same database connection safely. Existing await store.initialize() and
await store.close() remain supported; initialize_sync() and close_sync() are
available to synchronous hosts. MemoryManager.from_config() initializes the
backend, and applications must call manager.close() or await manager.aclose()
after every manager/fork using the shared backend has finished.
Initialization through from_config() and synchronous memory methods block the
caller. Async hosts can offload the factory with asyncio.to_thread() and use the
store's async methods for nonblocking I/O. See Memory.
Firefly exposes Pydantic AI 2.x capabilities= for native tools and other agent
capabilities. Existing framework tools still use tools= / toolsets=. See the
native-capabilities example. Firefly explicitly
keeps end_strategy="early"; "graceful" and "exhaustive" are opt-in.
Native instrumentation defaults to version 5; legacy configured versions 1
through 4 map to 5, and version 6 can be selected explicitly. Pydantic AI 2.x
removed the separate log-event mode: instrumentation_event_mode remains accepted
for configuration compatibility but has no effect, and events are represented as
span attributes. Update telemetry queries and exporters that consumed the old log
events. Content inclusion remains controlled by instrumentation_include_content.
The [evaluation] extra retains Ragas 0.2.6
(>=0.2.6,<0.2.7) with langchain-community>=0.3.31,<0.4. This is a compatibility
pin: Ragas 0.4.3 requires Instructor, whose
jiter<0.15 constraint conflicts
with OpenAI 3.19's jiter>=0.16.
Intermediate Ragas 0.2.7–0.3.1 also patches asyncio during import.
The pinned extra resolves with the upgraded SDK. Framework metric adapters pass
LLM and embedding clients through Ragas evaluate(...) so its wrappers apply
correctly. Offline tests exercise the actual metric implementations and check
that importing Ragas preserves the active event loop; this does not claim support
for the latest Ragas release or validate live judge quality.
Why. Tool parameter schemas were declared with a string type_annotation
resolved through a tiny built-in map that only understood str/int/float/
bool/list/dict and dropped generic element types, enums and nested
models. The LLM therefore saw lossy schemas (list[str] became an untyped array,
Literal[...] lost its enum). ParameterSpec now takes a real Python type
object, which pydantic-ai introspects directly for a full-fidelity schema.
The string type_annotation field and its resolver (_TYPE_MAP,
_resolve_param_type) have been removed — there is one way to declare a type.
# Before
ParameterSpec(name="tags", type_annotation="list[str]") # array with no item type
ParameterSpec(name="body", type_annotation="dict[str, Any] | None")
# After
ParameterSpec(name="tags", python_type=list[str]) # array of strings
ParameterSpec(name="body", python_type=dict[str, Any] | None)
ParameterSpec(name="mode", python_type=Literal["fast", "slow"]) # now a real enumpython_type defaults to str, so ParameterSpec(name="q") is a string param.
The second argument is now the real type, not a string:
# Before
ToolBuilder("weather").parameter("city", "str", description="City name")
# After
ToolBuilder("weather").parameter("city", str, description="City name")All built-in tools were migrated; if you subclassed BaseTool or built tools with
ParameterSpec/ToolBuilder, replace each string type with the real type. There is
no compatibility shim — a stray type_annotation= keyword is simply ignored by the
model (it is no longer a field).
A tool can now receive pydantic-ai's RunContext (agent deps, usage, retry count,
messages) by setting takes_ctx=True. The context is delivered to _execute as the
keyword-only _ctx; guards and the cache never see it, so it cannot poison a
cache key.
class SetPriority(BaseTool):
def __init__(self) -> None:
super().__init__("set_priority", takes_ctx=True,
parameters=[ParameterSpec(name="level", python_type=str)])
async def _execute(self, *, _ctx=None, **kwargs):
return f"{_ctx.deps}: {kwargs['level']}" # reach the agent's depsDecorated tools (@firefly_tool) that declare a RunContext-first parameter already
get it natively — no change.
fireflyframework_agentic.tools now re-exports pydantic-ai's RunContext and the
native toolset combinators (FilteredToolset, PrefixedToolset, RenamedToolset,
CombinedToolset, WrapperToolset, PreparedToolset, ApprovalRequiredToolset),
plus to_pydantic_handler(tool). Import them from the tools package instead of
reaching into pydantic_ai.toolsets. ToolKit.as_toolset() now also forwards each
tool's description to the model (it was previously dropped).
Why. Workflow sub-agents used to run as a bare pydantic_ai.Agent, bypassing the
whole framework. They now run through a FireflyAgent by default, inheriting the
middleware chain (logging, prompt/output guards, cost guard, caching, observability,
explainability, validation, retry), the 429 rate-limit retry loop, the global usage
tracker / budget gate, and model fallback.
What changes for you:
- Cost is now tracked globally for workflow sub-agents, and a configured global
budget_limit_usd(BudgetGate) now applies to them — a workflow can raiseBudgetExceededErrorwhere it previously would not. Per-runWorkflowBudgetis unchanged and still applies on top. - The model-resolution contract is unchanged: you still pass
model=per call ordefault_model=on the runner; nothing new is required.
To keep the old lightweight, bare-pydantic_ai.Agent behaviour, opt out explicitly:
from fireflyframework_agentic.workflows import DefaultAgentRunner
await my_workflow(args, runner=DefaultAgentRunner()) # the lightweight pathA single workflow can route different calls to different configured agents — for multi-model sub-agents and per-task cost optimisation:
@workflow(name="triage")
async def triage(args, ctx):
label = await agent("classify the ticket", using="cheap-classifier")
return await agent("write the resolution", using="premium-writer")
await triage(args, runner=FireflyAgentRunner()) # both resolved from the registryusing accepts a FireflyAgent instance or a registry name. With a non-
FireflyAgentRunner runner it raises a clear error.
Why. Tool approval was a bespoke guard (ApprovalGuard(callback)) that ran inside
Firefly's guard chain and raised ToolError on a denied call — a synchronous, all-or-nothing
gate with no pause/resume, metadata, or per-call granularity, parallel to pydantic-ai's own
deferred-tools protocol. It has been removed in favour of the native protocol
(requires_approval, DeferredToolRequests/DeferredToolResults, ApprovalRequired,
ApprovalRequiredToolset).
Previously, ApprovalGuard(callback=approve) wrapped a tool and blocked denied
calls. Remove that guard and mark the tool requires_approval=True. This complete
replacement pauses, asks for an explicit decision, and resumes the same history:
import asyncio
from fireflyframework_agentic.agents import FireflyAgent, is_deferred
from fireflyframework_agentic.tools import DeferredToolResults, firefly_tool
records = {"42": {"title": "Draft invoice"}}
@firefly_tool("delete_record", description="Delete a local example record", requires_approval=True)
async def delete_record(record_id: str) -> str:
removed = records.pop(record_id, None)
return "Deleted" if removed is not None else "Record not found"
agent = FireflyAgent(name="approved-delete", tools=[delete_record])
result = await agent.run("Delete record 42")
if is_deferred(result):
approvals = {}
for call in result.output.approvals:
answer = await asyncio.to_thread(input, f"Approve {call.tool_name} {call.args}? [y/N] ")
approvals[call.tool_call_id] = answer.strip().lower() == "y"
result = await agent.run(
message_history=result.all_messages(),
deferred_tool_results=DeferredToolResults(approvals=approvals),
)For the old inline, non-pausing behaviour (a callback decides programmatically), pass
FireflyAgent(approval_handler=...) — wired as a native HandleDeferredToolCalls capability.
See Human-in-the-Loop Tool Approval.
Also: guard denials (validation, rate-limit, sandbox) now raise ToolGuardError instead of a
plain ToolError. ToolGuardError subclasses ToolError, so existing except ToolError
handlers keep working.
- Replace every
type_annotation="..."withpython_type=<real type>inParameterSpecandToolBuilder.parameter(name, <type>). - (Optional) Enrich types now that they are real —
list[str],Literal[...], nested models — for better LLM tool schemas. - (Optional) Add
takes_ctx=Trueto tools that need agent deps/usage. - Review workflows for global cost/budget effects now that sub-agents run through
FireflyAgent; passrunner=DefaultAgentRunner()if you want the old path. - Import toolset combinators /
RunContextfromfireflyframework_agentic.tools. - Replace
ApprovalGuardwithrequires_approval=True+ the native pause/resume flow (is_deferred()+deferred_tool_results=), or an inlineapproval_handler=. - If you matched on
ToolErrorfrom guard denials specifically, note it is now theToolGuardErrorsubclass (still caught byexcept ToolError).