Agent frameworks

LangChain and LangGraph

vincs[langgraph] brings the relevant part of your Vinc graph into LangGraph nodes and LangChain agents. It reads only; writing back happens in a step you place after a person approves.

If the agent has to follow its role's contract (what it must never do, its gates, the tools it may not use), use role mode (prefetch_role). Role mode stops the run when Vinc cannot be read; context mode goes on without context. The table below compares them.

Install

pip install "vincs[langgraph]"

Requires Python 3.11 or later. This extra installs LangGraph, langchain-core and LangChain 1.0 or later, including the create_agent middleware. Set VINC_API_KEY to a read-only key (vinc_ro_); only record_episode and write_package need a write key, and with a read-only key they raise before anything is sent.

This page describes vincs 0.1.5, which carries vinc-langgraph 0.1.9. The separate vinc-langgraph package has the same names under vinc_langgraph.

Role mode or context mode

VincContextMiddleware runs in role mode when you pass prefetch_role, and in context mode when you do not.

ModeWhen Vinc is unreachable or the daily limit is used upReads per run (hub calls)Tools the role disallowsPinning
Role mode (prefetch_role)The run stops before the model is called. There is no retry, and any failed read stops it the same way.Once per run, on its first model call, reused until the next human message: 1 call for the role, 2 per required document (the document and its node), 1 brief and 1 search.Kept out of the tools each model call sees, and a call to one gets a tool message saying the role does not allow it. Names must match exactly; entries that match no tool give one warning per run.Optional, from vincs 0.1.5: with prefetch_pin set to a contract hash from role(), a run whose role has changed raises VincRoleChanged before the model is called. Without it each run reads the role as it is when the run starts. Either way a run keeps its contract to its end.
Context mode (no role)A warning is logged and the model call goes ahead without context. A wrong key or space raises.1 brief per question, plus a search when it ties or finds nothing, reused for cache_seconds (60). After an outage, the empty result is kept for those 60 seconds too.Not applied: there is no role.Not applicable: each lookup reads the graph as it is.

The per-run read and the tool rule come with vincs 0.1.4. With vincs 0.1.3 the middleware reads the role before every model call and does not apply disallowed_tools. In a graph node, prefetch raises like role mode and get_vinc_context returns None like context mode.

In a graph node

Call get_vinc_context at the start of the node that calls the model, and add the block to the system message with build_system_message.

from langchain.chat_models import init_chat_model
from vinc import VincClient
from vinc.integrations.langgraph import build_system_message, get_vinc_context

llm = init_chat_model("openai:gpt-5")
vinc = VincClient()                                    # VINC_API_KEY=vinc_ro_...

def review(state):
    block = get_vinc_context(vinc, state["messages"])   # newest human message
    system = build_system_message(block, "You review design changes.")
    return {"messages": [llm.invoke([system, *state["messages"]])]}

In a create_agent agent

Add VincContextMiddleware and every model call gets the block in its system message.

from langchain.agents import create_agent
from vinc.integrations.langgraph import VincContextMiddleware

agent = create_agent("openai:gpt-5", middleware=[VincContextMiddleware()])
agent.invoke({"messages": [{"role": "user", "content": "Why are tokens OKLCH?"}]})
ArgumentDefaultMeaning
client, async_clientmade from the arguments belowPass your own clients to share them.
api_key, base_url, spaceVINC_API_KEY, Vinc, personalUsed when no client is passed.
search_fallbackTrueWiden with a search when nothing matched or nodes tied.
max_chars, max_excerptspackage default, 3Size of the block.
cache_seconds60In context mode, an agent calls the model several times per question when it uses tools. The block for a question is reused for this long instead of costing a call each time.
prefetch_role, prefetch_max_charsnone, 24000Read this role and its evidence once per run, before the first model call (below).
tool_calls_per_run4How many times one run may call vinc_brief and vinc_search together; None never caps. A run is everything after the newest human message.
prefetch_pinnoneA contract hash from role(). A run whose role has changed stops before the model is called (below). Needs prefetch_role.

Tools the model can call

create_vinc_tools gives the model two read-only tools: vinc_brief (resolve a question to one node with its material) and vinc_search (search text and nodes, with a limit). Pass a sync client, an async client, or both.

from vinc import VincClient
from vinc.integrations.langgraph import create_vinc_tools

tools = create_vinc_tools(VincClient())   # vinc_brief and vinc_search, read only
agent = create_agent("openai:gpt-5", tools=tools)

Record after a person approves

Put record_episode in the node after an interrupt(), and give only that node a write key. The graph below reads, drafts, waits for a person and records only what was approved.

context → review → approve (interrupt) → record
import os
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt
from vinc import VincClient
from vinc.integrations.langgraph import get_vinc_context, record_episode

reader = VincClient()                                   # vinc_ro_ key
writer = VincClient(os.environ["VINC_WRITE_KEY"])      # vinc_sk_ key, record step only

class State(TypedDict, total=False):
    question: str
    context: str | None
    draft: str
    approved: bool
    about: list[str]
    written: dict | None

def context(state):  return {"context": get_vinc_context(reader, state["question"])}
def review(state):   return {"draft": draft_review(state["question"], state["context"])}  # your model call
def approve(state):  return {"approved": bool(interrupt({"draft": state["draft"]}))}
def record(state):
    if not state["approved"]:
        return {"written": None}
    return {"written": record_episode(writer, state["draft"], about=state["about"])}

g = StateGraph(State)
for name, fn in [("context", context), ("review", review), ("approve", approve), ("record", record)]:
    g.add_node(name, fn)
g.add_edge(START, "context"); g.add_edge("context", "review")
g.add_edge("review", "approve"); g.add_edge("approve", "record"); g.add_edge("record", END)
app = g.compile(checkpointer=InMemorySaver())

cfg = {"configurable": {"thread_id": "review-1"}}
paused = app.invoke({"question": "Does the new button meet contrast?", "about": []}, cfg)
app.invoke(Command(resume=True), cfg)                  # a person approved

Retry the record step

From vincs 0.1.4, record_episode, arecord_episode, write_package and awrite_package take create_only and operation_id and pass them to the client unchanged.

Run the agent as a Vinc role

Pass prefetch_role to VincContextMiddleware. On the run's first model call it reads that role's whole contract, the full text of the documents the role says to read first, and Vinc's material on the task, with node ids the model can cite, and every model call of the run gets that block. The model gets the evidence without having to decide to look it up.

from langchain.agents import create_agent
from vinc import VincClient
from vinc.integrations.langgraph import VincContextMiddleware, create_vinc_tools

reader = VincClient()                                  # vinc_ro_ key
agent = create_agent(
    "openai:gpt-5",
    tools=create_vinc_tools(reader),                   # optional, for further lookups
    middleware=[VincContextMiddleware(
        client=reader, prefetch_role="concept:agent-reviewer-judgement",
        prefetch_max_chars=120000,
    )],
)

Prefetch reads the complete role contract and each required document by its declared document id once per run; a new human message starts a new run and reads again. Each document must have complete text and a verified content hash; missing, partial or unverified documents raise prefetch_spec_unresolved before optional task lookup. Authentication and service errors propagate, so the run stops before the model is called. The combined prefetch_max_chars budget defaults to 24,000 characters. Large roles need a larger budget, such as the 120,000-character budget in this example. prefetch_budget_exceeded stops the read instead of truncating the contract or required documents. Your code picks the role and its budget. Helpers use max_chars for this same budget.

To keep the agent on the contract you reviewed, pin the role. Read it once, keep its hash, and pass it as prefetch_pin. When someone has changed the role (its props, title, topic or relations), the run's first model call raises VincRoleChanged with the current rev and hash in details, and the model is not called. Moving the pin to the new hash is your decision. The pin covers the role contract, not the text of the documents it lists. Graph node helpers take the same value as pin. It comes with vincs 0.1.5.

pin = reader.role("concept:agent-reviewer-judgement")["hash"]    # after you reviewed it
middleware = VincContextMiddleware(
    client=reader, prefetch_role="concept:agent-reviewer-judgement",
    prefetch_max_chars=120000, prefetch_pin=pin,
)

Check the final answer against the role

OutputGate checks the final answer outside the model, against the role's gates, never_checks and person_only. It answers pass, block or interrupt, and the middleware raises OutputGateStopped on the last two. It calls no model and writes nothing.

from vinc import OutputGate
from vinc.integrations.langgraph import create_output_gate_middleware

role = reader.role("concept:agent-reviewer")           # read fresh for this run
gate = OutputGate(role["props"])
agent = create_agent("openai:gpt-5", middleware=[create_output_gate_middleware(gate)])
agent.invoke({"messages": [{"role": "user", "content": "Review the proposed change."}]})

Ask a person before a person-only step

Have the model node store a vinc.RoleOutput in role_output, add the node from create_person_only_node, and compile with a checkpointer. When the answer asks for a person-only action, the node pauses with a native interrupt. Resume it with the person's decision; the model is not called again.

from langgraph.types import Command
from vinc.integrations.langgraph import create_person_only_node

graph.add_node("approval", create_person_only_node(gate, role_id=role["id"], run_id="review-1"))
graph.add_edge("model", "approval")

paused = app.invoke(inputs, cfg)
pending = paused["__interrupt__"][0].value
app.invoke(Command(resume={"request_id": pending["request_id"], "approved": True}), cfg)

Declare vinc_approval in the state schema and deliver only that checked result. A rejected answer has nothing to deliver, and approval never overrides a block. Approval releases this answer only; it does not run or permit any tool.

Hand off to another agent

Passing only citation ids loses the first agent's conclusion. Instead, the first agent writes one package Record with the question, the conclusion and each claim's evidence, and the next agent reads it by id.

from vinc.integrations.langgraph import get_vinc_package_context, write_package

handoff = write_package(                               # first agent, vinc_sk_ key
    writer, "When does the dashboard become stale?", "Keep the 36 hour threshold.",
    claims=[{"claim": "Stale after 36 hours.", "evidence_ids": ["decision:threshold"]}],
    produced_by="agent:analyst", run_id="analysis-1", space="personal",
)
block = get_vinc_package_context(reader, handoff["package_id"], space=handoff["space"])

Name the space; the evidence must already exist there. The next agent rereads the package and checks every evidence id with its own key each time, so deleted or unreadable evidence is marked as not citable. For a create_agent agent bound to one handoff, add PackageProvider(package_id, space=..., client=reader) as middleware.

Structured review, one retry

run_review(model, messages) (async arun_review) asks the model for a ReviewOutput: a verdict of ACCEPT or RETURN, findings with citations, and person-only requests. A malformed answer gets one retry. A second failure raises UnparsedReview instead of passing as an empty review; hand that to a person.

from vinc.integrations.langgraph import UnparsedReview, run_review

try:
    review = run_review(model, "Review this draft against its cited rules")
except UnparsedReview:
    raise                                              # stop here and hand it to a person

Every helper

Import them from vinc.integrations.langgraph. A helper that reads or writes takes the client first; one whose name starts with a takes an AsyncVincClient and is awaited.

HelperParametersWhat it does
get_vinc_context, aget_vinc_contextclient, messages, search_fallback, max_chars, max_excerptsA block for the newest human message, or None.
prefetch, aprefetchclient, role_id, task, space, max_chars, pinThe role's required reading and task evidence. Raises like role mode.
get_vinc_package_context, aget_vinc_package_contextclient, package_id, space, max_charsA handoff package, checked again with this key. space is required.
build_system_messageblock, basebase with the block added, as a SystemMessage.
last_user_textmessagesThe text of the newest human message.
create_vinc_toolsclient, async_client, max_charsThe read-only tools vinc_brief and vinc_search.
record_episode, arecord_episodeclient, title, summary, about, date, domain, tags, space, episode_id, create_only, operation_idOne episode. Needs a write key.
write_package, awrite_packageclient, question, conclusion, claims, produced_by, run_id, space, package_id, create_only, operation_idOne handoff package Record. Needs a write key; space is required.
VincContextMiddlewarethe table aboveContext for each model call of a create_agent agent. clear_cache() drops what it kept.
PackageProviderpackage_id, space, client, async_client, api_key, base_url, max_charsA handoff package for each model call of a create_agent agent.
run_review, arun_reviewmodel, messages, native_structured, config, optionsA ReviewOutput, with one retry.
create_output_gate_middlewaregate, evidenceChecks the final answer of a create_agent agent.
evaluate_output_node, require_output_passstate, gate, evidenceThe same check as a graph node. The second raises instead of routing.
create_person_only_nodegate, role_id, run_id, evidenceAn approval node that pauses with interrupt.

Over MCP instead

If you would rather not add a package, langchain-mcp-adapters can reach the Vinc MCP server. Keep the tool list to reads.

import os
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({"vinc": {
    "transport": "http", "url": "https://mcp.vincs.io/mcp",
    "headers": {"Authorization": f"Bearer {os.environ['VINC_API_KEY']}"},
}})
tools = [t for t in await client.get_tools()
         if t.name in {"vinc_brief", "vinc_search"}]