Skip to content

Haystack

Available · Tool per spec · confirmation hook · pip install "agent-adapter[haystack]" (haystack-ai)

Functions

Function (agent_adapter.integrations.haystack) Returns
haystack_tool(spec, *, executor=None) one haystack.tools.Tool with async_function and a run_sync function
haystack_tools(registry, *, executor=None) one per registry tool exported to haystack
confirmation_hook(port, registry) a before_tool Agent hook backed by an approval port
INTEGRATION get_integration("haystack"); bind_approval(port) = the hook, resolving specs from the Agent's tools

Agent.run_async awaits the tool; Agent.run (sync) runs it through run_sync. Haystack shows results to the model as JSON text.

Wiring

def build_agent(chat_generator: Any) -> Agent:
    hook = confirmation_hook(CallbackApproval(reviewer), registry)  # asks before send_invoice
    return Agent(
        chat_generator=chat_generator,
        tools=haystack_tools(registry, executor=Executor(registry)),
        hooks={"before_tool": [hook]},
    )

Approval

The hook runs Haystack's own ConfirmationHook with a port-backed strategy, so rejected calls are removed from the pending calls and the model is told why.

sequenceDiagram
  participant A as Haystack Agent
  participant H as confirmation hook
  participant P as ApprovalPort
  participant T as Tool
  participant Ex as Executor
  A->>H: before_tool(state)
  H->>P: request(tool, arguments, call_id) per pending call
  alt approved
    H->>H: one-use token, kept beside the State
    A->>T: invoke_async(**args, State)
    T->>Ex: run_spec(..., ctx.approval = token, if it matches)
  else denied / pending
    H->>A: rejection message as the tool result
  end
Case What happens
Approved a one-use token (60 s) bound to the run, that pending call id, the tool and the validated arguments; only a call matching one of the last message's pending calls takes it, and the executor does not ask again
Token not taken in its step dropped when that hook runs the next step
Several confirmation hooks on one Agent each keeps its own tokens; a hook only drops its own
No call id on the pending call denied (the approval could not be bound to it)
Denied the model sees {"error": {"kind": "approval-denied", ...}}
Port raises ApprovalPending the call is rejected with approval required: <id>
Edited arguments not supported: an edit is treated as a denial (logged at WARNING)
No hook the executor's own confirm rule and port decide (no port: denied)

The hook and its strategy hold a live port, so Agent.to_dict() raises IntegrationError.

Calls and errors

Item Behaviour
Call id the tool injects the Agent State (parameter _aa_state) and takes the id of the matching pending tool call, one claim per id
Run one run id per Agent run (State); tokens and claims live beside the State, never in state.data, so outputs and snapshots carry none
ToolArgsInvalid, ToolError, ApprovalDenied, GuardrailBlocked, RunLimitExceeded returned as {"error": {"kind", "message"}}
Other errors raised; Haystack's ToolInvocationError becomes an error tool result
CancelledError propagates