Hayhooks V2: durable execution for pipelines and A2A agents - #253
Closed
mpangrazzi wants to merge 29 commits into
Closed
mpangrazzi wants to merge 29 commits into
mpangrazzi wants to merge 29 commits into
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. |
mpangrazzi
force-pushed
the
hayhooks_v2
branch
from
August 12, 2026 09:14
0485782 to
683423f
Compare
sjrl
reviewed
Aug 19, 2026
Comment on lines
+229
to
+230
| # The module uses postponed annotations, while Haystack validates hook | ||
| # signatures with ``inspect.signature`` rather than resolving hints. |
Contributor
There was a problem hiding this comment.
Just FYI this is fixed in Haystack 3.1 from PR deepset-ai/haystack#12185
sjrl
reviewed
Aug 19, 2026
| *hooks.get("after_run", []), | ||
| FunctionHook(function=checkpoint_after_run, async_function=checkpoint_after_run_async), | ||
| ] | ||
| self.pipeline.hooks = hooks |
Contributor
There was a problem hiding this comment.
Not a big deal, but editing Hooks this way does skip hook validation. In Haystack 3.1 you could use the Agent.clone method and pass in your new hooks there as an alternative. Only if you want the init to fire again to do the validation checks.
…ct test with cancellation contract
Four verified defects in the SSE execution stream, each probed against a real Redis before and after the fix: - read_chunks had no count bound, so a client reattaching from 0-0 materialized the whole log in one list: 10 000 x 64 KB = 640 MB at default limits. Both backends now read at most CHUNK_READ_COUNT per call and the generator's existing catch-up loop carries the rest. - append_chunk only set a TTL when the control hash was still readable, so an append past the terminal TTL created a chunk key that never expired (measured pttl -1). Treat a missing control the same as a terminal one. The reference backend refuses the append instead, since its cleanup has already run. - durable_redis_socket_timeout accepted sub-second values while the stream blocks 500 ms in XREAD, and redis-py applies socket_timeout to that read: any value under 0.5 turned every stream into an immediate `error` event. Floored at one second. - A quiet stream reread the full execution record on every 500 ms poll, three round trips each, measured at 9.3 reads/s per viewer against a `waiting` execution that can be parked for hours. Recheck the lifecycle every fourth quiet poll instead: 2.7 reads/s, terminal event within ~2 s. Docs pick up the parts that stay as they are: a `waiting` execution keeps its stream open, an EventSource must close() on the terminal event, and the two chunk limits multiply into the per-execution Redis footprint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_it_durable_chat_with_website failed on CI with "durable test server did not become ready". wait_for_server rode wait_until's in-process default budget of 200 attempts x 10 ms = 2.00 s, but booting a server that imports Haystack and loads a pipeline measures 1.88 s locally -- a 6% margin that a slower runner loses. Fixed in the helper rather than the one call site that failed: all six subprocess boots across the durable tests shared that budget. A dead process still fails on the first poll, so the wider ceiling only costs time when the server really is coming up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_http_disconnect_pipeline_task[cancel] failed on CI asserting that a cancelled pipeline task can never reach completion. Releasing the blocked component races the delivery of the cancellation to its await point, and which one wins depends on how many loop turns Haystack's async pipeline takes internally, so the assertion is version-dependent rather than contractual. The shield case still asserts completion, and both cases still assert that a task is detached only when shielding was asked for, which is the part of the contract this test exists to pin.
Contributor
Author
|
closing in favour of #264 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack
Merge in order: #253, then #258, then #259. PR #258 adds durable A2A trace correlation; PR #259 adds its dashboard classification and presentation.
Durable execution and managed A2A Agents
This PR adds durable execution for Haystack 3 Pipelines and Agents. It accepts typed work, persists it before returning, runs it outside the request, and recovers it from safe checkpoints after a worker or process disappears.
Highlights
GET /{pipeline}/executions/{id}/streamendpoint carrying best-effort display chunks at token rate plus the terminal projection. Chunks live in a bounded Redis stream deliberately outside the durable fence, so a dropped token can never fail or replay a run.durable_revisiongate prevents incompatible queued or waiting work from resuming against changed code.DurableRuntime, configure it withDurableSettings, and include a typed durableAPIRouterwith their existing authentication dependencies.See Hayhooks durable engine and Temporal for the capability and tradeoff comparison.
Portability and FastAPI integration
The durable engine can be embedded through
hayhooks.durablewithout depending on Hayhooks server startup. Its public surface includesDurableRuntime,DurableSettings,DurableDeployment,ExecutionStore,ExecutionStoreProvider, the built-in memory and Redis providers, durable contracts and exceptions, andcreate_durable_router(); Redis remains optional at import time.An application creates its runtime and deployment, starts and closes the runtime in its FastAPI lifespan, and includes the router under any prefix. The router exposes typed submit, inspect, cancel, resume, and stream operations for one deployment. A normal sync or async FastAPI dependency can return a stable owner ID, so the host keeps control of authentication and authorization. Passing
owner_id_dependency=Noneexplicitly selects unscoped access.Hayhooks now consumes that same public router instead of maintaining a second REST implementation. Its internal shim only installs and removes per-deployment routes safely. REST, A2A, and MCP lifespans operate on the runtime instance they own rather than the process-global singleton.
Standalone runtimes own and start only their attached deployments; they do not inspect the process-global pipeline registry or require private server-loader flags. Runtime, deployment, provider, and store use one settings snapshot, conflicting settings fail before deployment, and provider replacement is locked once a provider or deployment candidate exists. The runtime owns provider shutdown.
See Embedding the runtime for the complete authenticated FastAPI example, lifecycle contract, unscoped-access warning, and Redis configuration.
Diff breakdown for reviewers
The PR is broad because it ships the engine together with recovery tests, runnable examples, operations documentation, A2A integration, and the portable FastAPI adapter. Of the 15,395 additions, 7,726 (about 50%) are tests, examples, or documentation.
Suggested review order:
src/hayhooks/durable/engine.py,backend.py,store.py,redis.py, andreference.pyfor lifecycle, storage, and chunk-log correctness.manager.py,context.py,adapters.py,runtime.py,settings.py, andfastapi.pyfor execution, configuration, Haystack boundaries, the public HTTP adapter, and the SSE stream route.server/durable/routes.py,server/utils/deploy_utils.py, and the REST/A2A/MCP lifespans for runtime ownership and dynamic route lifecycle.server/a2a/durable_executor.pyandredis_task_store.pyfor the A2A-specific projection and recovery layer.examples/durable_chat_with_websiteand its tests are the streaming reference material.Supported features
Durable REST execution
BasePipelineWrapperimplementsrun_durable()orrun_durable_async(). Hayhooks invokes that method for each durable execution with aDurableContextand typed Pydantic request.context.run_pipeline()/context.run_pipeline_async()for checkpointed Pipeline work, orcontext.run_agent()/context.run_agent_async()for Agent work.POST /{pipeline}/run-durableGET /{pipeline}/executions/{id}GET /{pipeline}/executions/{id}/streamPOST /{pipeline}/executions/{id}/cancelPOST /{pipeline}/executions/{id}/resumeIdempotency-Keyreplays the same operation safely and rejects reuse with different input.durable_revision, allowing queued and waiting work to be checked against checkpoint-relevant deployment code.Pipeline checkpoints
Pipeline wrappers opt in through
context.run_pipeline(..., checkpoint_at=[...])orcontext.run_pipeline_async(..., checkpoint_at=[...]).For every named boundary, Hayhooks asks Haystack to stop at a public
Breakpointand immediately persists the returnedPipelineSnapshotbefore that component executes. If Haystack exposes a snapshot with aPipelineRuntimeError, Hayhooks persists that snapshot too.On recovery, Hayhooks rebuilds the
PipelineSnapshot, passes it back to the Pipeline, and starts from the saved scheduler state. Haystack skips the already-completed upstream component visits; work after the last checkpoint is replayed.Agent checkpoints
Managed Agents use the public Haystack hook surface. Hayhooks installs synchronous and asynchronous versions of these hooks for the active durable execution:
before_runStatewhile keeping fresh per-run tools and hook context.before_llmafter_toolon_exitcontinue_run.after_runThe serialized Agent checkpoint excludes live
toolsandhook_context; the current deployment recreates them for the recovered run. Checkpoints and progress are saved together as a durable execution transition.Redis recovery
chunksstream per execution, and two indexes:runnablefor queued work andlease-expiryfor active claims.TIME, optimistic transactions, and fenced transitions keep ownership safe across replicas.nonterminal,runnable, andlease_expirycounts.Streaming chunks
DurableContext.stream_chunk()(andstream_chunk_sync()for worker threads) append one display chunk outside the durable fence: a singleXADDthat never contends with the lease heartbeat.HAYHOOKS_DURABLE_MAX_STREAM_CHUNK_BYTES, 64 KB default) or a backend blip drops that chunk and logs it, never failing or replaying the execution.HAYHOOKS_DURABLE_MAX_STREAM_CHUNKSbounds the log per execution (10 000 default);0disables chunk production while leaving the endpoint working.id:and the producingattemptin the payload. Reconnecting clients resume fromLast-Event-IDwithout a gap; a retried attempt re-streams from its checkpoint, so clients reset their buffer whenattemptincreases.completed/failed/canceledevent carrying the same projection as the inspect route. A break after headers are sent ends in an SSEerrorevent instead, which clients treat like a dropped connection.examples/durable_chat_with_website, which detaches mid-answer, reattaches, and survives a process restart.Managed long-running A2A Agents
AgentinA2APipelineWrapperis exposed as a durable A2A Agent. Hayhooks supplies the durable worker, queue, execution record, checkpointing, progress projection, and Redis integration.GetTaskand list requests project the latest durable state directly.Execution model
The lifecycle is a pure reducer over a compact execution control record:
The store atomically persists each reducer plan and its derived Redis indexes. The durable manager owns worker polling, lease heartbeats, retries, and shutdown draining; Haystack adapters own the Pipeline snapshot and Agent hook integration. Display chunks are the one deliberate exception: they bypass the reducer entirely.
See the durable operations guide.
Validation
OPENAI_API_KEY.