Skip to main content
Prerequisites. A VisIQ account (sign in) with a harness key from Settings → Harness Keys, Node 20+ (or Python 3.9+ for the Python path), and an OPENAI_API_KEY (the sample below calls an OpenAI model — any provider works). Full setup and fixes: Before you start · Troubleshooting.
Add action governance, retrieval governance, and a full audit trail to a LlamaIndex.TS agent workflow by passing your agent to visiq(). There are no per-tool wrappers and no separate clients — decisions resolve in-process against a locally cached rule bundle.

Install

Set environment variables

.env

Wrap your agent

runStream() is governed identically to run(), and each run gets a fresh session id so the dashboard correlates every decision in that run.
Retrieval governance contract. The harness governs a LlamaIndex workflow at the tool boundary: each tool’s call() is gated, and per-document filtering applies to tools that return document-shaped results — array items with a string pageContent, text, or content field, plus optional metadata that retrieval rules match on (classification, data categories, …). A retriever wired directly into the workflow (e.g. index.asRetriever()) returns node objects the filter does not recognize — expose retrieval as a tool that maps retrieved nodes to { text, metadata } documents before returning them, as in the snippet above.

Python

LlamaIndex ships a Python framework, and so does VisIQ: the visiq wheel, published on PyPI and compiled from the same governance core as the TypeScript harness. Pass your agent to visiq.govern(). VisIQ installs itself at the agent’s own tool-dispatch chokepoint, the _call_tool method every LlamaIndex workflow agent routes a model-chosen tool call through, so there are no per-tool wrappers. It governs FunctionAgent, ReActAgent, CodeActAgent and a multi-agent AgentWorkflow, and it reaches tools you append to agent.tools after govern() and tools served by a tool_retriever. Every decision is made locally, in-process against a cached rule bundle.
Pin llama-index-workflows below 2.24. With llama-index-core 0.14.24, the workflow engine’s 2.24.0 release fails every agent run with TypeError: unhashable type: 'FunctionAgent', with or without VisIQ installed, and 2.23.3 does not. Drop the pin once LlamaIndex ships a fix.
The visiq wheel reads its configuration from the process environment and does not auto-load a project .env (that is the TypeScript harness). Export the variables above, or load them yourself, before calling govern().
govern() returns the same agent, so it also works as an expression. It is idempotent: governing an agent twice installs governance once. Governance is installed on that one object, never on the class, so other agents in the same process are unaffected. Every tool call is decided before the tool runs. A deny means your function is never called: the block message is returned as the tool’s result, so the model reads why and adjusts course instead of the run crashing. A mask hands your function only the redacted arguments, nested ones included. Tools named in retrieval_tools also have their result governed before it reaches the model: a list of dicts, a list of LlamaIndex Document / TextNode / NodeWithScore objects, or a JSON string of a list is filtered per document, with restricted documents dropped and sensitive fields redacted. A result the retrieval facet cannot split into documents (a dict envelope, the synthesized Response a QueryEngineTool returns) is withheld rather than passed through, so expose retrieval as a tool that returns the retrieved nodes. Running several agents as an AgentWorkflow? Govern the workflow itself, govern(workflow, agent_id="support-bot"). The workflow dispatches tool calls through its own _call_tool, so governing a member agent alone does not govern the calls the workflow makes. Its built-in handoff tool is framework control flow and is not gated; every other tool is.
Governance decisions are local and synchronous, but they run off your event loop, so a human-approval hold cannot freeze the rest of your asyncio app. The tool itself still runs in the workflow’s own task, so cancelling a run also cancels the tool it was running.
On the first run the agent and its tool surface are registered with VisIQ, and each run’s decisions are delivered to the dashboard when the agent finalizes that run.
What govern() does not cover on LlamaIndex. A CodeActAgent runs model-written code, and the functions that code calls are not gated one by one; the code execution itself is the governed call. A run that ends through early_stopping_method="generate" or by raising never reaches the agent’s finalize, so its decisions are delivered with the next batch (after 20 buffered events, on the next run, or at interpreter exit) rather than when that run ends. Calling a tool object directly (tool.acall(...)) outside the agent is not an agent tool call and is not governed. Need finer control than the whole agent? The per-call Governor.gate_tool primitive is documented in the Python SDK Reference.
A runnable manual-wiring example (the same agent, with each tool wrapped in Governor.gate_tool by hand) lives in examples/llamaindex-agent-py. The LangChain and OpenAI Agents SDK quickstarts have Python peers too.

Run this on AWS Bedrock

LlamaIndex ships its own first-party Bedrock integration, so moving the Python agent above onto Bedrock is a provider swap: install the package and hand FunctionAgent a BedrockConverse instead of OpenAI. The govern() call is unchanged:
Credentials come from the standard AWS chain (AWS_PROFILE, instance role, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY). Governance is untouched: the gate fires at the same point, the same rules match, the same rows land in the audit trail.
On older LlamaIndex releases, construct the agent inside the coroutine that runs it. On the 0.11 and 0.12 lines LlamaIndex bound its workflow to the event loop that was alive when the agent was constructed, so a FunctionAgent built once at module scope and then driven by a fresh asyncio.run(...) per turn died with RuntimeError: no running event loop, and because the tools then never ran, tool-level assertions passed vacuously. On llama-index-core 0.14.24 with llama-index-workflows 2.23.3 the module-scope pattern above runs correctly (measured over four consecutive asyncio.run turns). Building the agent inside main() is safe on every release.
Deploying to Bedrock AgentCore Runtime needs nothing extra either — it hosts your process, so install visiq in your image and set VISIQ_API_KEY in the runtime environment. The governance and its locally cached bundle come along unchanged. If instead you use a managed harness, AWS owns the agent loop and these in-process gates do not apply; see AWS Bedrock for the two chokepoints that still work there.

What happens at runtime

Wrapping is safe to try immediately — new agents start in monitor mode (observe-only) until you flip them to enforce on the Control AI → Agents page.
  • Decisions are local. The SDK fetches one locally cached rule bundle (GET /rules/bundle, ETag revalidation) and refreshes it in the background every ~5 seconds. Tool calls evaluate in-process; the only decision-path network call is waiting on a human approval.
  • Fail-open by default, loudly — strict deny is opt-in. Real policy outcomes always enforce regardless of failMode: an explicit rule deny, the operator kill-switch, and an in-core mask/redact that cannot be applied (it downgrades to deny) all block. A brand-new agent whose mode has never been confirmed cold-starts in monitor (observe, never block). But a harness-internal failure — an unreachable backend, the governance core unavailable, a refused wire dialect — by default proceeds ungoverned with a loud [VisIQ] FAIL-OPEN stderr report (plus a structured failOpen flag) so a VisIQ outage never disrupts your agent (owner decision, 2026-07-15). Set failMode: 'closed' on visiq() or VISIQ_FAIL_MODE=closed to make those harness-internal failures deny instead. This page also documents the Python SDK, which honours the same posture. The visiq wheel fails OPEN on a harness-internal failure with the same loud [VisIQ] FAIL-OPEN report, and honours VISIQ_FAIL_MODE=closed to block instead. A policy deny, a required mask that cannot be applied, and a HITL approval that cannot be confirmed still block in Python exactly as in TypeScript.
  • Denials are returned, not thrown. A blocked call hands the model [VisIQ decision=deny code=<rule-code>] This tool call was NOT executed: it was denied by policy (<description>). VisIQ is a security harness installed by your developer. Report this reason to the user verbatim; do not invent a different one. as the tool’s output, so the agent reads it and adjusts course.
  • Approvals pause the call. An approval_required decision holds the tool while a human decides via Slack or Email (Microsoft Teams delivery is built server-side; its connector card is coming soon) — the SDK polls for up to 120 seconds (VISIQ_HITL_TIMEOUT_MS), then fails closed.
  • Mask proceeds, redacted. A mask decision runs the tool with the named arguments redacted; retrieval redaction masks document fields before the model sees them.
  • Covered from the first call. Every workspace ships a curated catalog of 36 default rules. Uncovered actions permit by default — no surprise breakage — and the per-operation-type default can be tightened in settings.

Verify it’s working

Run the agent once, then open the dashboard:
  • Control AI → Agents: your agent appears automatically (monitor mode) with a live last-seen heartbeat.
  • Control AI → Agent Events: a decision row for every governed tool call, with the matched rule and outcome.
  • Control AI → Escalations: pending approvals. Route them to Slack or Email under Integration → Connectors (Human-in-the-loop) — Microsoft Teams delivery is built and its connector card opens shortly.

Next steps

Full Quickstart

All supported frameworks and what happens behind the scenes.

SDK Reference

Complete visiq() API, options, framework detection, and error behavior.