v3.1.0-rc1
Pre-releaseRelease Notes
v3.1.0-rc1
Upgrade Notes
-
exit_reasonis now a reserved state key onAgent. If you defined a customstate_schemakey namedexit_reason, rename it: the Agent now raises aValueErrorat initialization when a reserved key is redefined. -
Agent.state_schemanow contains the user-provided state schema, exactly as passed to__init__. Previously it contained the resolved schema, which also includedmessagesand the keys the Agent manages internally (step_count,token_usage,exit_reason, ...). If you were readingagent.state_schemato inspect the effective runtime schema, use the new public attributeagent.resolved_state_schemainstead. -
DocumentMAPEvaluatorscores can change because average precision now uses all unique, valid ground-truth comparison values as its denominator and credits each value at most once. Re-baseline evaluations that relied on the previous scores. -
PipelineSnapshot.pipeline_state.inputschanged shape. It used to store one flattened value per socket,{component: {socket: value}}. It now stores the pipeline's internal inputs, keeping the component that sent each one in the order it arrived:{component: {socket: [{"sender": ..., "value": ...}]}}.PipelineStategained aninputs_formatfield recording which of the two shapes a snapshot uses. The same applies toBreakpointException.inputs, which returns that field.You are affected if you read
pipeline_state.inputs(orBreakpointException.inputs) directly, for example to display or post-process a snapshot. Resuming a pipeline withPipeline.run(pipeline_snapshot=...)is not affected, and neither is code that only passes snapshots around or persists them.To adapt, read the input from the list and take its
value:inputs = snapshot.pipeline_state.inputs["serialized_data"] # before value = inputs["my_component"]["my_socket"] # now value = inputs["my_component"]["my_socket"][0]["value"]
A socket that received inputs from several senders has one list item per input, each recording the
senderthat produced it.Snapshots written by earlier versions of Haystack have
inputs_formatset toNoneand keep the flattened shape, so branch on that field if you need to handle both. -
Loading a serialized
OutputAdapterorConditionalRouterwhoseunsafeinit parameter is set totruenow raisesDeserializationErrorunless the pipeline is loaded in unsafe mode. Pipelines that legitimately rely on an unsafeOutputAdapter/ConditionalRouterembedded in serialized data must now load withPipeline.load(..., unsafe=True)(orPipeline.loads/Pipeline.from_dictwithunsafe=True). -
Passing
window_size=0toSentenceWindowRetriever.runorSentenceWindowRetriever.run_asyncnow raises aValueErrorinstead of silently using thewindow_sizeset in the constructor. You are affected if you passwindow_size=0at runtime, either directly or from an upstream component in a pipeline. If you were relying on0to mean "use the value from the constructor", omit the argument (or passNone) instead:retriever = SentenceWindowRetriever(document_store=document_store, window_size=3) # Before: silently used window_size=3 retriever.run(retrieved_documents=docs, window_size=0) # After: omit the argument to use the constructor value retriever.run(retrieved_documents=docs)
-
InMemoryDocumentStore.get_metadata_field_unique_values(and its async counterpart)'ssearch_termparameter now matches against the metadata field's own value (case-insensitive substring) instead of the document's content. Callers relying on the previous content-matching behavior will need to filter documents by content themselves before calling this method. -
The Agent now warms up its hooks before every run, not only the first one, as it already does for Tools and Toolsets. If your hook has a
warm_up()that does expensive setup (opening a client, loading a model), make it return early once done, for exampleif self._client is not None: return. -
Haystack can call
warm_up()on Tools and Toolsets more than once, for example before every run. PreviouslyToolsetabsorbed repeated calls with an internal_is_warmed_upflag; that flag is gone and every call now reaches yourwarm_up(). If your custom Tool or Toolset does expensive work there (connecting to a server, loading a model), or relied on the_is_warmed_upattribute, guard with your own state and return early, for exampleif self._client is not None: return.
New Features
-
Added a
link_formatparameter to bothPyPDFToDocumentandPDFMinerToDocumentcomponents, matching the existing functionality inDOCXToDocument. Links are parsed from PDF annotations and appended at the bottom of the page content. -
Added experimental context compaction for the
Agent.CompactionHookruns before LLM calls and shortens the conversation when it reaches a configured fraction of the model's context window.The first built-in strategy,
SlidingWindowCompactor, preserves leading system messages, the latest user task, and as much complete recent conversation as the target allows. It removes complete historical turns first, and only when removing every historical turn is insufficient does it remove individual Agent steps from the current task. It replaces removed history with a short omission note, left where the removed messages used to sit: directly after the leading system messages when only historical turns were removed, and directly after the latest user message when the current task's own steps were removed. Only one note is ever present, because a later compaction folds an earlier one into its replacement.from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIResponsesChatGenerator from haystack.hooks.compaction import CompactionHook, SlidingWindowCompactor hook = CompactionHook( compactor=SlidingWindowCompactor(), context_window=400_000, compact_at=0.7, compact_to=0.4, ) agent = Agent( chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4-nano"), tools=[web_search], hooks={"before_llm": [hook]}, )
The hook uses provider-reported context usage when available and locally estimates the request otherwise, including tool schemas. Leave headroom above
compact_atfor the next reply and its tool results.SlidingWindowCompactortreats an assistant message and its following tool results as one step, so a tool call is never separated from its results. Historical turns are likewise kept or removed in full, so an assistant reply is not retained without the user message it answers. It can also land above the requested target rather than under it, because leading system messages and the current task are never removed andmin_keep_stepsholds on to the newest Agent steps whatever their size, so a long system prompt or one large tool result can leave the conversation well over the target. Compaction is lossy: removed messages cannot be recovered or summarized by this strategy. Implement theCompactorprotocol to provide a custom strategy.CompactionHookandSlidingWindowCompactoremit anExperimentalWarningand may change without a deprecation cycle. -
Agentnow returns anexit_reasonoutput reporting why the run stopped, making it easier to route the Agent's output downstream (for example with aConditionalRouter). It is one of:"text"(the model returned a reply with no tool calls), the name of the tool that satisfied a tool exit condition (in which caselast_messageis that tool's result), or"max_agent_steps"(the Agent hitmax_agent_stepsbefore meeting an exit condition). The reason is also available to hooks viastate.get("exit_reason"), so anafter_runhook can, for instance, append a fallback answer when the step budget is exhausted. -
Add
OpenAITokenCounter, which uses OpenAI's input token counting API to return model-specific counts for HaystackChatMessageobjects and optional tool schemas. Unlike local estimates, it supports OpenAI's exact accounting for request formatting, images, files, and tools.Here is an example:
from haystack.dataclasses import ChatMessage from haystack.token_counters import OpenAITokenCounter counter = OpenAITokenCounter("gpt-5-mini") count = counter.count([ChatMessage.from_user("Hello!")])
-
Added
haystack.token_counters: aTokenCounterprotocol for estimating how many tokens a list ofChatMessageobjects occupies, with two implementations.Providers report token usage only after a call, and only for the call as a whole, so anything that needs a size beforehand - deciding whether a conversation still fits a model's context window, or how much of it to drop - has to estimate one.
from haystack.dataclasses import ChatMessage from haystack.token_counters import ApproximateTokenCounter, TiktokenCounter messages = [ChatMessage.from_user("Hello, how are you?")] # No dependencies: estimates from text length. ApproximateTokenCounter(chars_per_token=4.0).count(messages) # Closer for OpenAI models; needs: pip install tiktoken TiktokenCounter(encoding="o200k_base").count(messages)
ApproximateTokenCounterneeds nothing installed and estimates from text length at a configurablechars_per_token.TiktokenCountercounts with OpenAI's byte-pair encoder, which is closer for OpenAI models but requirestiktokenand drifts on other providers; it raises at construction when the dependency is missing, and loads its encoding on first use.Neither can measure an image or a file, since a tokenizer only sees text and providers derive an image's cost from its dimensions. Both charge a flat
tokens_per_imageandtokens_per_fileinstead, counting images a tool returned inside its result as well as those a message carries directly. Raise those values if you send large images or long documents.Tool schemas are sent alongside the messages and consume tokens too, so
counttakes an optionaltoolsargument to have them included:counter.count(messages, tools=[my_tool])
Implement
TokenCounterto count differently - for instance against a provider's own token-counting endpoint, which is the only way to have images counted exactly. -
Added the experimental
ToolResultPruningCompactor. It reduces Agent context usage by replacing older, large tool results with short placeholders while preserving tool-call/result structure. Results from a configurable number of recent tool-calling Agent steps remain intact, including parallel results from those steps.from haystack.hooks.compaction import CompactionHook, ToolResultPruningCompactor compaction_hook = CompactionHook( compactor=ToolResultPruningCompactor( min_keep_steps=2, min_tokens=200, ), context_window=400_000, compact_at=0.7, compact_to=0.4, )
-
Add an
Agent.clone()method that returns a new Agent with the same configuration, optionally replacing some init parameters:variant = agent.clone(system_prompt="Answer in German."). -
Added
AgentTool, a Tool that wraps a HaystackAgent, allowing it to be used as a tool by anotherAgent. It is a building block for multi-agent systems: anAgentspecialized in one task becomes a tool that anotherAgentcan delegate to. The callingAgentonly sees the final reply, so all the steps the wrappedAgenttakes stay out of its context. Sensible defaults make this work out of the box: the task is delegated as a single user message and comes back as text.Example:
from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIResponsesChatGenerator from haystack.dataclasses import ChatMessage from haystack.tools import AgentTool, ComponentTool from haystack_integrations.components.websearch.serperdev import SerperDevWebSearch researcher = Agent( chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4-mini"), system_prompt="You are a research specialist. Investigate the task and report your findings.", tools=[ ComponentTool( component=SerperDevWebSearch( top_k=3, ), name="web_search", description="Search the web for current information on any topic", ), ], ) research = AgentTool( agent=researcher, name="research", description="Research a question on the web and report the findings", ) coordinator = Agent( chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4"), tools=[research], system_prompt="You coordinate specialists. Delegate research questions, then answer the user.", ) result = coordinator.run([ChatMessage.from_user("What are the latest developments in the Haystack framework?")]) print(result["last_message"].text)
-
Haystack components that use a document store now provide
closeandclose_asyncmethods for releasing resources. These methods are available on:AutoMergingRetriever,CacheChecker,DocumentWriter,FilterRetriever, andSentenceWindowRetriever. If the underlying Document Store does not implement the corresponding method, callingcloseorclose_asynchas no effect. -
Add a content-free
haystack.agent.hooktracing span for every Agent hook invocation. Each span identifies the hook point, hook name, and hook type, allowing hook latency and failures to be attributed without tracing the potentially large AgentState.CompactionHookalso adds its configured compaction strategy, estimated context size, whether compaction was triggered, its token target, and whether the compactor returned a replacement.
Enhancement Notes
- Extracted
DOCXLinkFormatto a reusableLinkFormatEnum inhaystack/components/converters/utils.py.DOCXLinkFormatis now an alias for backward compatibility. - The
Agentnow tracks an approximate current context-window size in its internalStateundercontext_tokens, refreshed after every LLM call with that reply's prompt-plus-completion tokens (normalized across theprompt_tokens/completion_tokensandinput_tokens/output_tokenskey conventions). Unliketoken_usage, which accumulates across the whole run,context_tokensis replaced each call. Hooks can read it viastate.get("context_tokens")— for example, abefore_llmhook that triggers context compaction once the value crosses a threshold. It is a best-effort snapshot: it is0when the generator does not report usage, and does not count messages appended after the latest call until the next call refreshes it. before_runhooks can now readstate.data["tools"]. The key was previously written only once the first step had started, so abefore_runhook hit aKeyError. It holds a snapshot of the tools available at that point, refreshed before every LLM call, so with a dynamic toolset such asSearchableToolsetabefore_runhook sees only the tools discovered so far.- Added an opt-in
strict_datetime_comparisonkeyword argument todocument_matches_filter,InMemoryDocumentStore, andMetadataRouter. When enabled, timezone-naive and timezone-aware datetimes never match each other. By default, mixed-awareness datetimes continue to be reconciled by copying the timezone from the aware value to the naive one, and this behavior is now consistent across equality, membership, and ordering operators. - Added a
filtersparameter toInMemoryDocumentStore.get_metadata_field_unique_values(sync and async), allowing the set of documents considered when computing unique metadata field values to be restricted. InMemoryDocumentStore.get_metadata_field_unique_valuesand its async counterpart now support pagination viafrom_andsizeparameters, matching the behavior of other Document Stores (e.g. Chroma).MockChatGenerator'sresponse_fncan now be tool-aware. If the callable accepts a second positional argument, it also receives thetoolspassed torun/run_async(aToolsTypeorNone), so a dynamic mock can build tool calls whose arguments follow the tool's parameter schema or route between the available tools. Existing single-argumentresponse_fncallables are unaffected and keep receiving only the messages.State.to_dictnow accepts askip_keysparameter to exclude specific keys from the output.- Add a tracing span per
ConfirmationStrategyrun toConfirmationHook. Eachhaystack.agent.hook.human_in_the_loop.strategyspan identifies the tool call it confirms and records the strategy type and the applied confirm, modify, or reject decision. When content tracing is enabled, spans also carry the arguments the strategy was run with and theToolExecutionDecisionit returned underhaystack.agent.hook.human_in_the_loop.strategy.inputandhaystack.agent.hook.human_in_the_loop.strategy.output; chat messages, the confirmation strategy context, and the AgentStateare not recorded by the hook. LLMEvaluator,LLMRanker,QueryExpander,LLMMetadataExtractor,LLMDocumentContentExtractorandLLMMessagesRouternow wrap their internalChatGeneratorcalls in ahaystack.chat_generator.runtracing span. These components do not returnChatMessageobjects, so the LLM token usage carried inreply.meta["usage"]was previously lost to tracers. The new span exposes the generator's replies via thehaystack.component.outputtag, so token usage is now visible in traces (requires content tracing to be enabled). When the generator runs across threads, the span is nested under the component's span.
Deprecation Notes
- Two ways of combining Toolsets are deprecated and will be removed in Haystack 3.2.0: the
+operator (toolset_a + toolset_b) and passing a Toolset toadd()(toolset_a.add(toolset_b)). Pass Toolsets as a list wherever tools are accepted instead:Agent(tools=[toolset_a, toolset_b]).
Security Notes
- Fixed a remote code execution vulnerability that could be triggered by loading an untrusted pipeline in default safe mode (
Pipeline.load/Pipeline.loads/Pipeline.from_dict, withoutunsafe=True). A malicious pipeline could either (a) setunsafe: trueon anOutputAdapterorConditionalRouterto disable the Jinja sandbox entirely, or (b) register thethread_safe_importimport primitive as a Jinjacustom_filtersentry to importosand execute arbitrary commands — bypassing the deserialization allowlist and Jinja sandbox. The fix denies import primitives during callable deserialization, refuses to honor a component'sunsafeflag while loading in safe mode, and hardens the Jinja sandbox (OutputAdapter,ConditionalRouter,PromptBuilder,ChatPromptBuilder) to block attribute access on module objects and calls into dangerous modules. - Closed an additional remote code execution vector in the deserialization control-plane hardening: the pipeline loading entry points (
Pipeline.loads/Pipeline.load/Pipeline.from_dict, which acceptunsafe=True) and the execute primitives (Pipeline.run/run_async/run_async_generator/stream) were still resolvable from the allowlistedhaystacknamespace. Bound as acustom_filtersentry on anOutputAdapterorConditionalRouter(which bypass the Jinja sandbox),Pipeline.loads(..., unsafe=True)let a pipeline loaded in default safe mode load a nested pipeline whose own filters (allow_deserialization_module,deserialize_callable) bind under the nested unsafe context, disarming the process-wide allowlist with"*"and invokingos.system. All of these entry points are now marked as deserializer-internal, so they can never be produced by deserializing untrusted data.unsafe=Truestill bypasses the check by design; there are no public API changes. - Fixed a remote code execution vulnerability that could be triggered by loading an untrusted pipeline in default safe mode (
Pipeline.load/Pipeline.loads/Pipeline.from_dict, withoutunsafe=True). Because the deserialization allowlist admits the wholehaystacknamespace, the deserializer's own allowlist-administration function (allow_deserialization_module) and its resolution helpers (deserialize_callable,deserialize_type,import_class_by_name) were themselves resolvable from serialized data. A malicious pipeline could registerallow_deserialization_moduleas a Jinjacustom_filtersentry (on anOutputAdapterorConditionalRouter), call it with"*"to disarm the allowlist process-wide, and then use the equally-resolvabledeserialize_callableto resolve and invokeos.system. Loading alone was enough to trigger this: a Jinja filter called with constant arguments runs while the component is being constructed, so the pipeline never had to be run. The same attribute walk could also reach the deserializer's mutable control-plane state directly — for example a filter bound to_extra_allowed_modules.append— to widen the allowlist persistently and stage a later attack. Relatedly, the handle resolver walked attribute names freely, so a handle could descend into object internals such as<function>.__globals__(a live module namespace, and via it__builtins__andeval/exec) or<type>.__subclasses__— classic sandbox-escape gadgets that stay inside an allowlisted module. The fix refuses to deserialize the deserialization control plane as a whole: the allowlist administration and resolution helpers (marked at definition time), everything defined inhaystack.core.serialization_security, and any bound method of the mutable allowlist/context state. It also refuses to traverse into dunder and frame/code attributes while resolving a handle. This applies to both the callable- and class-resolution paths, and is bypassed only when the pipeline is loaded withunsafe=True. - Harden
FileSystemToolResultStore.read()so it only reads references that resolve within the configured store root. This closes a boundary gap where callers could previously pass an arbitrary filesystem path toread()instead of a store-scoped reference returned bywrite().
Bug Fixes
-
Fixed the serialization of
PDFMinerToDocument. The component did not defineto_dict, so the default serialization fell back to reading the init parameters from same-named attributes. Since the layout parameters are stored inself.layout_params, they were silently serialized with their default values, for example a component created withchar_margin=0.5was serialized withchar_margin=2.0. Custom layout parameters are now preserved when a pipeline is serialized and loaded again. -
Cancel and await sibling retrieval tasks when a concurrent call fails in
MultiRetriever,MultiQueryTextRetriever, orMultiQueryEmbeddingRetriever. -
Fixed an infinite recursion in
CSVDocumentSplitterwhen nested row and column blocks were split together. -
Comparing two
Documentobjects with==now takes all metadata into account. Previously, two documents with different metadata could be considered equal if the metadata contained keys with the same names as document fields (such asidorcontent). -
Document.from_dict(document.to_dict())now correctly rebuilds any document. Previously, if the metadata contained keys with the same names as document fields (such asidormeta), this either raised an error or silently lost those metadata entries. -
Fixed
DocumentNDCGEvaluatorproducing NDCG scores outside the documented 0.0 to 1.0 range when the same document appeared more than once. A document retrieved multiple times used to be counted multiple times, pushing the score above 1.0; a ground truth document listed multiple times used to inflate the ideal gain, keeping a perfect retrieval below 1.0. Each distinct relevant document is now counted once, with the same relevance, in both the actual and ideal gain, so scores stay within range. -
Fixed
AnswerBuilderreturning referenced documents in a scrambled order instead of ascending source-index order. The referenced document indices were collected in asetand iterated directly, so documents were emitted in the set's internal hash-table order (e.g. citations [3] [10] [50] yielded documents ordered 10, 3, 50). This order was deterministic but did not match the intuitive source order. The referenced documents are now returned sorted by their source index. -
Fixed
serialize_typeanddeserialize_typeto correctly round-tripCallabletypes that declare an explicit parameter list, such asCallable[[int, str], bool]. Previously the parameter list was dropped during serialization (producing a malformed string liketyping.Callable[, bool]) and could no longer be deserialized. This affected components that serialize type annotations, for exampleConditionalRouterandOutputAdapterusing aCallableoutput type. -
Fix
DocumentMAPEvaluatorto include missed relevant documents in the average precision denominator and avoid crediting duplicate retrievals of the same document. -
Fixed
DocumentSplitterproducing chunks that were not present in the source document whensplit_thresholdwas set together withsplit_overlap. Merging a below-threshold trailing segment into the previous split re-appended the overlapping units, duplicating text. The overlap is now added only once. -
Fixed
EmbeddingBasedDocumentSplitter.run_asyncembedding through the synchronous path while recursively splitting chunks longer thanmax_length. Only the first pass was async: the recursion called the sync splitting helper, so the embedder's blockingrunran on the event loop for every over-long chunk. The recursion now embeds throughrun_asyncas well. -
Fixed
JSONConverterraising aKeyErrorinstead of logging its intended "Failed to extract text, skipping it" warning when a source is aByteStreamwithout afile_pathin itsmeta(for exampleByteStream.from_string(...), the exact usage shown in the component's own docstring examples). Affected error paths: invalid UTF-8 content, ajq_schemafilter that fails to apply, and malformed JSON content. -
Fixed
LinkContentFetcherrotating theUser-Agenton a cursor shared by every URL in the samerun()/run_async()call. The URLs are fetched concurrently, so a retry triggered by one of them advanced the user agent for the others, and each completed fetch reset the cursor for the requests still in flight — most retries went out with the un-rotated user agent. Each fetch now walks theuser_agentslist on its own, so a URL rotates exactly as documented no matter how many other URLs are fetched alongside it. -
Fixed
MarkdownHeaderSplittersilently dropping a trailing header that has no body text. Withkeep_headers=True(the default), a header at the end of the document whose only content is whitespace was buffered to prepend to the next chunk, but with no following chunk it was never emitted, so the split documents no longer reconstructed the original text. Such trailing headers are now emitted as a final chunk. -
Fixed
MarkdownHeaderSplittercollapsing blank lines that follow a header with no body text. Withkeep_headers=True, such headers were re-joined with a single newline when prepended to the next chunk, so the split documents did not reconstruct the original text. Chunk content is now sliced from the original text and is byte-exact. -
Fixed
MarkdownHeaderSplitterincluding surrounding whitespace in theheaderandparent_headersmetadata fields. The header text is now stripped; chunk content still keeps the header line's original whitespace. -
Fixed schema-based serialization of lists, tuples and sets holding mixed types. Previously the schema was derived from the first element only, so deserializing such a value raised an
AttributeErroror silently returned mis-typed data (for example an AgentStatefield or a pipeline breakpoint input holding[Document(...), "text", 3]). Mixed-type arrays now record one schema per position using the JSON SchemaprefixItemskeyword and round-trip correctly. Homogeneous arrays keep the exact same output as before, so existing snapshots still load. -
Fixed
MSGToDocumentraising aKeyErrorwhen converting aByteStreamsource that has nofile_pathin itsmeta(for example a bareByteStream(data=...), rather than a file path or a stream produced viaByteStream.from_file_path). Attachments extracted from such a source no longer include aparent_file_pathkey, since there is no source file path to record. -
Pipeline connections now always convert values in the same way. When a component output is connected to an input that accepts multiple types, Haystack is sometimes able to automatically convert the value, and more than one conversion may be possible. For example, a
ChatMessagewith text "hello" connected to an input annotatedstr | list[str]can be delivered either as plain text ("hello") or as a list containing the text (["hello"]). Previously the conversion strategy was chosen non-deterministically, so the same pipeline could return a different value across runs. The conversion strategy is now selected using a fixed priority: first, wrapping a value in a list or unwrapping a single-element list; second, converting betweenChatMessageandstr; and last, combining both conversions. -
Fixed
normalize_metadata(used by all file converters) returning the same dictionary object for every source whenmetaisNoneor a single dictionary. Each source now receives an independent copy, so mutating one source's metadata downstream no longer leaks into the others. -
OpenAIResponsesChatGeneratorno longer mutates theparametersschema of theToolobjects passed to it. Previously every run wroteadditionalProperties: Falseinto the user's liveTool.parameters, silently altering the tool for any other generator that shared the sameToolinstance and making serialization round trips unstable. -
OpenAIResponsesChatGeneratorno longer raisesIndexErrorwhen it is warmed up with an emptytoolslist. -
Fixed the parent of the
haystack.agent.step.toolspans when an Agent step invokes several tools. The parent span is now resolved once before the tools run, so all tool calls of a step appear as siblings. Previously each span asked the tracer for the currently active span from inside its own concurrent invocation, which made the tool calls after the first one appear nested under a sibling tool call. -
Fixed the
haystack.pipeline.output_datatracing tag being empty. The tag was set at the start ofPipeline.run/run_asyncfrom the still-empty outputs, and since tracing backends coerce a tag value when it is set, the recorded output was always an empty dictionary. It is now set once the run completes so it reflects the final pipeline outputs. The tag is also gated behind content tracing (HAYSTACK_CONTENT_TRACING_ENABLED), consistent with the component-level input/output tags. -
Fixed resuming a
Pipelinefrom apipeline_snapshotthat was taken on a component's second or later visit, which failed withPipelineComponentsBlockedError: Cannot run pipeline - all components are blocked. A snapshot stored only the values of the pipeline's inputs and dropped the information about which component had sent each one, so on resume every restored input looked like it came from outside the pipeline, and such an input can only trigger a component on its first visit. Snapshots now record the sender of each input. Snapshots created by earlier versions of Haystack behave as before, so re-create them to resume anywhere in a looping pipeline. -
Fixed a resumed
Pipelinepassing malformed inputs to the component the snapshot was taken on, whenever that component ran more than once after the resume, for example inside a loop. Every visit after the first reused the handling meant only for the paused visit and skipped the regular input consumption, so a variadic component could receive a bare value where it expected a list, raising errors such asTypeError: object of type 'int' has no len()from aBranchJoiner. This affected snapshots taken at any visit count, including the first. -
Fixed
QueryExpanderreturning duplicate queries when the chat generator repeats an expansion. Generated queries are now deduplicated while preserving first-seen order, so repeated expansions no longer trigger redundant retrievals or consume the requested expansion budget. Bothrunandrun_asyncare affected. -
RecursiveDocumentSplitter's word-mode fixed-size fallback no longer counts a run of whitespace (e.g. a double space, tab, or page break) as a word, so it no longer produces chunks smaller thansplit_length. It also no longer emits a whitespace-only chunk when the text ends in whitespace right after a chunk boundary; that trailing whitespace is now attached to the previous chunk instead.This changes the exact chunk boundaries and chunk count produced by the word-unit fallback for any text containing such whitespace runs. Documents already split and indexed under the old behavior will produce different chunks if re-split after upgrading, so re-index any document store that relies on stable chunk boundaries from this fallback path.
-
Fixed an issue where
PipelineBase.remove_componentdid not reset auto-variadic socket flags (is_lazy_variadicandwrap_input_in_list) on input sockets when components or connections were removed. -
Fixed
Pipeline.remove_componentleaving dangling references to the removed component on the sockets of its neighboring components. Previously, removing a component reset only its own sockets, so a surviving neighbor kept the removed component's name in its input socket'ssenders(or output socket'sreceivers). This corrupted introspection and validation:Pipeline.inputs()hid a now-unconnected mandatory input, and feeding that input directly could raise a spurious "already connected" error. The removed component's name is now stripped from its neighbors' sockets as well. -
SentenceWindowRetriever.runandSentenceWindowRetriever.run_asyncnow validate an explicitly providedwindow_size=0instead of treating it as unset and falling back to the constructor value. -
Fixed the schema-aware serialization helper used for pipeline snapshots and
AgentState(_serialize_value_with_schema) so it no longer silently passes unsupported objects through as if they were serialized. Values such asdatetime,bytes,complexand arbitrary objects without ato_dictmethod were previously stored unchanged and mislabeled as strings, which broke JSON storage and round-tripping of snapshots. Unsupported values now raise aSerializationError, and the callers that build snapshots (pipeline breakpoints andState.to_dict) catch it to omit only the offending field while keeping the rest of the payload resumable. -
Added support for serializing and deserializing
frozensetvalues in_serialize_value_with_schema. Afrozensetnow round-trips back to afrozensetinstead of being dropped. -
Fixed
serialize_type/deserialize_typefortyping.Literal. Previously aLiteraltype hint was serialized with its values rendered as bare tokens (e.g.typing.Literal[yes, no]), which failed to deserialize, and values that looked like type names (e.g.Literal["int", "str"]) were silently turned into types on the round-trip. The values are now serialized withrepr()and read back withast.literal_eval, so aLiteraltype used by a component (such asOutputAdapterorConditionalRouter) round-trips correctly through pipeline serialization. -
Fixed an
AttributeError: 'str' object has no attribute 'items'raised bycreate_tool_from_function, the@tooldecorator, andComponentToolwhen a tool parameter is namedproperties. Keys inside a JSON schemapropertiesmapping are property names and are no longer misinterpreted as schema keywords when stripping the auto-generatedtitlekeywords. -
Fixed
create_tool_from_function, the@tooldecorator, andComponentToolcorrupting a tool's JSON schema when the stringtitleappears as a name rather than as a schema keyword. Stripping the auto-generatedtitlekeywords no longer deletes entries of$defs,definitions,patternProperties,dependentSchemasordependentRequired(which would leave a$refdangling or silently drop a validation rule), and no longer editstitlekeys insidedefault,const,enumorexamplesvalues, which are instance data and part of the tool's contract. -
Fixed
_ToolsetWrapper.__getitem__(used when combiningToolsets with+) raisingIndexErrorfor negative indices, unlike a plainToolset. Indexing a combined toolset now behaves consistently with a list ofTools, as documented. -
Fixed serialization of types that contain
...(Ellipsis), such as variadic tuples (tuple[int, ...]) andCallable[..., X]. Previouslyserialize_typerendered the...as the literal string"Ellipsis", whichdeserialize_typethen rejected as a non-type builtin, so a component using such a type (for exampleOutputAdapter(output_type=tuple[int, ...])) could be serialized but not deserialized, breakingPipeline.loads()/Pipeline.load(). These types now round-trip correctly, and pipelines serialized by older versions (which emitted"Ellipsis") can still be loaded. -
Fixed
ConfirmationHookapplying a Human-in-the-Loop decision to the wrong tool call when a customConfirmationStrategyreturns a decision with a missing or incorrecttool_call_id. Each decision is now bound to the tool call for which its strategy ran, and ID-bearing decisions are no longer matched to a different call by name. Haystack's existing requirement of exactly one decision per tool call is now explicitly enforced. Each matched decision is consumed after use, so a missing, unused, or reused decision raises aValueErrorinstead of being silently misapplied. -
DocumentJoinerandAnswerJoinernow resolvetop_kconsistently and validate it. Previously, a runtimetop_k=0was treated as "unset" and silently fell back to the instance'stop_k, instead of returning an empty list as requested. Both components now:- Raise a
ValueErrorat initialization iftop_kis notNoneand is less than or equal to0. - Raise a
ValueErrorat runtime iftop_kpassed torun()is negative. - Return an empty list when
run()is called withtop_k=0, regardless of the instance's configuredtop_k.
- Raise a
-
Fixes
MetaFieldRankersilently treating a runtimetop_k=0as unset and falling back to the value configured at initialization. Runtime values that are not greater than zero now raise aValueErroras documented. -
Fixed
PythonCodeSplitterlosing identifying context for oversized functions, methods, or classes. When a unit is too large and falls back to line-based secondary splitting, only the first resulting piece naturally retains the sourcedef/classline; every piece now includes aqualified_namefield inmetaidentifying the function, method, or class it came from. -
Fixed
RecursiveDocumentSplitternot setting thesource_idmeta field on the chunks it produces. It wrote onlyparent_id, while every other splitter in the library (DocumentSplitter,CSVDocumentSplitter,EmbeddingBasedDocumentSplitter,HierarchicalDocumentSplitter,MarkdownHeaderSplitterandPythonCodeSplitter) writessource_id. Components that follow that convention therefore rejected its output:SentenceWindowRetrieverreadssource_idby default and raises when it is absent, so it failed with "The retrieved documents must have 'source_id' in their metadata." on a pipeline that worked with any other splitter. Chunks now carrysource_idas well asparent_id, which keeps its previous value for callers already reading it. -
Tool functions defined in a module using
from __future__ import annotationsare now inspected correctly byAgent. Postponed annotations are stored as strings, so a parameter annotated withStatewas not recognized and the liveStateobject was not injected into the tool call. The annotations are now resolved before they are inspected. -
MarkdownHeaderSplitterandCSVDocumentSplitternow deep-copy the metadata of the document they split, matchingDocumentSplitter. Previously they copied it shallowly, so nested values such as a list undermeta["tags"]were shared between every chunk and with the input document, and editing one chunk's metadata changed all the others.HierarchicalDocumentSplitterhad the same problem on its root node, which kept references into the input document's metadata. -
Keep an Agent's execution counter in sync with
step_countrestored by abefore_runhook, so restarted Agents continue from the saved step instead of resetting the count.
💙 Big thank you to everyone who contributed to this release!
@Aarkin7, @anakin87, @anxkhn, @aquib8112, @Aryan-Pardeshi, @atikulmunna, @bharadwaj-pendyala, @bilgeyucel, @bogdankostic, @camgrimsec, @chuenchen309, @davidpavlovschi, @davidsbatista, @DhanushPillay, @DivyaNarahari97, @erikos, @GovindhKishore, @hxaxd, @immuhammadfurqan, @iridescentWen, @jaideeppyne, @julian-risch, @kacperlukawski, @KXHXK, @LHMQ878, @LK-maker-007, @lntutor, @manjunathbhaskar, @mittalpk, @MVS-source, @onatozmenn, @otiscuilei, @pcbeingused333, @rautaditya2606, @sjrl, @sohumt123, @Solaris-star, @TimurRakhmatullin86, @vidigoat, @vinkiYu, @winklemad, @yaodong-shen