Repository navigation
Integrations Haystack
Experimental: this works and is tested, but may change and gets less hardening than the stable core.
tracekit.integrations.haystack.Tracekit is a Haystack Agent hook that gates every tool call the agent makes. At
before_tool it asks the Tracekit signer to decide each call the model asked for, before any of them runs (an ask
waits for a person); at after_tool it records each outcome. Haystack 3 runs tools only inside an Agent (it has no
ToolInvoker component), so this covers the tool calls of a pipeline's agents too.
pip install tracekit-ai "haystack-ai>=3.4,<3.5"Tested with Haystack 3.4, which needs Python 3.11+. OpenAIChatGenerator comes with haystack-ai; other model
providers come in their integration packages. The hook itself needs none.
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from tracekit.integrations.haystack import Tracekit
from tracekit.sdk.client import Client
with Client().run(agent="support-agent") as run:
tk = Tracekit(run)
agent = Agent(chat_generator=OpenAIChatGenerator(), tools=[close_account],
hooks={"before_tool": [tk], "after_tool": [tk]})
agent.run([ChatMessage.from_user("close ACC-9")])Register the same tk at both hook points, last in each list. tracekit.instrument() doesn't wire Haystack: build the
hook yourself. In a process that calls instrument() (it also records the model calls made through the OpenAI,
Anthropic and Google Gen AI SDKs), pass it its handle: Tracekit(tracekit.instrument()). An AsyncRunHandle works as
well as a RunHandle. Then tracekit last exports and verifies the run.
Every agent needs the hook, a sub-agent included: the outer agent's hook decides the call to an AgentTool, and only
the sub-agent's own hooks decide the calls the sub-agent makes (give it the same tk). A runnable offline example (a
MockChatGenerator, no API key): examples/v2/haystack.
-
Decides the model's own arguments. The signer decides each
ToolCallunder itsid, with itsarguments(the JSON the chat generator parsed,{}for none). A call without an id (some generators make them, andToolCalls you build without one) is given one,haystack-<hex>, that its tool result then carries. The values Haystack adds from the agent's state (inputs_from_state, aStateparameter) are not the model's and are not decided. An approval is bound to the arguments and used once; under the executor (T2), a call whose approved arguments are not the model's is refused, not run with either. -
Never runs a refused call. A deny, a rejected or unanswered approval, a refused consume, a signer refusal, an
outage under a closed fail mode and any error of Tracekit's own take the call off the step: the model reads
Tracekit's reason as an error tool result in its place (as Haystack's
ConfirmationHookrejects a call) and the run goes on. Under an open fail mode a call runs unrecorded during an outage, with a warning. -
Records failures as failures. A tool that raises reaches the model as Haystack's error result, as before, and is
recorded as
status: error. A result goes to the signer as the model reads it: Haystack sends the model a tool's return as text, JSON text for a dict or a list, which is parsed back, so an untrusted tool's values reach provenance. It records every result after the step's calls, wherever anafter_toolhook listed before it moved it. -
Decides each call on its own. The calls of one step are decided at once, those the model gave one id one after
the other; a call id the model uses again, in the same step or a later one, is decided as the call's next
attemptand recorded with its own result. -
Notices a later change. A
before_toolhook listed after Tracekit that changes a decided call's tool or arguments doesn't stop it running, but the call is recorded asstatus: error, "the call's tool or arguments changed after the decision". Hooks listed before it (Haystack'sConfirmationHookamong them) may change the calls: it decides them as they leave those hooks. -
Waits for people. An
askwaits up to 5 minutes (the signer's cap for one wait) in the hook; then the call is refused with the approval's state, and an unanswered approval is withdrawn. So is one whoserun_asyncis cancelled while it waits. -
Covers every tool Haystack runs.
Agent.runandAgent.run_async, sync and async tools,ComponentTool,PipelineTool,AgentTooland the tools of aToolsetall reach the same hooks.
At before_tool, before deciding the calls, the hook records the message: its reasoning (with OpenAI's
encrypted_content), text and tool calls under the ids they are decided by (the one Tracekit gives a call that has
none). The message itself is kept only when the signer's content vault is on; without it the record holds a commitment. A message that can't be recorded only warns. Which frameworks and providers return readable reasoning, and how to turn it on: reasoning.
- With
raise_on_tool_invocation_failure=True, a tool that raises ends the agent run beforeafter_tool: the calls of that step are not completed and read asdecided_not_completedonrun.final. Keep the default (False), where the model reads the error and the call is recorded as one. A tool output that fails to merge into the agent's state (outputs_to_state) ends the run the same way, whatever that setting. - An
after_toolhook listed before Tracekit that drops a result (or the whole step) leaves that calldecided_not_completed. - Tools the model provider runs itself (
OpenAIResponsesChatGenerator's built-in tools such asweb_searchor a remotemcpserver) never reach the hooks; they show only in the model's responses, which autotrace records underinstrument(). - Values a tool writes to the agent's state (
outputs_to_state) and a later tool reads from it (inputs_from_state, aStateparameter) are not the model's arguments: they are not decided, sofrom: untrustedrules don't see an untrusted tool's values passed that way. - A
before_toolhook listed after Tracekit can add a call, which then runs undecided. List Tracekit last. - A call without an id is decided under
haystack-<hex>, which its result then carries. With the LLM gateway the model's tool use has an id of the gateway's own, so the two pair only by their arguments (paired_by_args), never when two calls of a turn are alike. - An
askholds its whole step: Haystack runs a step's calls once everybefore_toolhook has returned, so the step's allowed calls wait for the answer too. - A refused call's error result is not sent to the agent's
streaming_callbackand not counted intool_call_counts: Haystack streams and counts only the calls it runs. - In a process that calls
instrument(), which wires MCP'sClientSession, a tool frommcp-haystackis decided twice: by the hook under its Haystack name and by the wired session (mcp-haystackcalls it on an event loop of its own, which the hook can't mark as decided). - Each signer call, and an
ask's wait, holds a thread of asyncio's default executor (in a syncAgent.run, the hook runs on an event loop of its own, in a thread); many asks waiting at once can starve it. - The hook holds a live run, so an agent or pipeline holding it can't be serialized (
to_dict,dumpsraiseSerializationError): build it in code.
Generated from docs/ at v1.4.1. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: a custom agent
- Quickstart: LangChain and LangGraph
- Quickstart: MCP client
- Quickstart: OpenAI Agents SDK
- Coding agents other than Claude Code
- tracekit doctor
- Walkthrough: Tracekit end to end
How it works
- How Tracekit works
- Overview
- The life of a tool call
- The evidence format
- Verification and assurance
- Witnesses and monitoring
- Policy
- Approvals
- Detection and containment
- Capture and completeness
- Custody
- Provenance and tracekit why
- Deployment and isolation
- Storage, durability and performance
- Integrations
- Security model
- Architecture
- tracekit why: architecture
Integrations
- Integrations
- Adapters
- OpenTelemetry
- Remote clients
- Kubernetes agent-sandbox
- agentgateway
- agentsh and Tracekit
- Browser Use
- Claude Agent SDK
- Claude Code's sandbox
- Codex's sandbox
- CrewAI
- Docker MCP Gateway
- E2B
- Falco
- Gemini CLI's sandbox
- Google ADK
- Haystack
- LangChain v1
- LiteLLM
- LlamaIndex
- MCP client
- microsandbox
- Microsoft Agent Framework
- OpenAI Agents SDK
- NVIDIA OpenShell
- Pydantic AI
- sandbox-runtime's proxy on its own
- Strands Agents
- Tetragon
- Vercel AI SDK
- Wassette
Policy and approvals
- Policy reference
- Approvals
- Findings
- Reading the model's reasoning
- Incidents and containment
- Webhooks
Deploy and operate
- Deploying the signer
- The signer as a container
- The signer on Kubernetes
- The compose stack
- Identity
- Tool executor
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- tracekit why: causal investigation and replay
- Migrating
- Platforms
Verify the evidence
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- What Tracekit guarantees
Security and privacy
- Threat model: the v2 signer
- Threat model: the v1 laptop signer
- HTTP security checklist
- Threat-model review packet
- Privacy
- Known limits and trade-offs
- What's stable
Compliance
- Compliance mapping
- EU AI Act
- ISO/IEC 42001
- NIST AI RMF and the Generative AI Profile
- OWASP Top 10 lists for LLM and agentic applications
Evaluation
Demos
- Demos
- Agent approvers within bounds
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- Prompt injection held by provenance
- Why: which input caused the action
Help