Agent frameworks

Microsoft Agent Framework

vincs[agent-framework] is a context provider for Microsoft Agent Framework. It adds the relevant part of your Vinc graph to each run, and writes only when your code asks.

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[agent-framework]"

Requires Python 3.11 or later. This extra installs agent-framework-core 1.8.1 or later alongside the SDK and account CLI. A read-only key (vinc_ro_) is enough for the providers and tools; only record_episode and write_package need a write key.

This page describes vincs 0.1.5, which carries vinc-agent-framework 0.1.8. The separate vinc-agent-framework package has the same names under vinc_agent_framework.

Role mode or context mode

VincContextProvider runs in role mode when you pass prefetch_role, and in context mode when you do not. Both read once per run, in before_run.

ModeWhen Vinc is unreachable or the daily limit is used upReads per run (hub calls)Tools the role disallowsPinning
Role mode (prefetch_role)before_run raises, so the run stops before the model is called. There is no retry, and any failed read stops it the same way.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 result saying the role does not allow it. Names must match exactly; entries that match no tool give one warning per run. Hiding needs ChatMiddlewareLayer inside FunctionInvocationLayer in the chat client, as in the built-in clients; with any other client the model still sees the tools, calls are still refused, and each run logs a warning.Optional, from vincs 0.1.5: with prefetch_pin set to a contract hash from role(), a run whose role has changed fails in before_run with 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 run goes ahead without context. A wrong key or space raises.1 brief, plus a search when it ties or finds nothing. None for a message shorter than min_query_chars.Not applied: there is no role.Not applicable: each run reads the graph as it is.

The tool rule comes with vincs 0.1.4; vincs 0.1.3 does not apply disallowed_tools. With expose_tools=True, the model's own lookups add up to 4 calls per run in either mode.

Add context to every run

Add VincContextProvider to context_providers. Before each run it looks up the newest user message in Vinc and, when something matched, adds a block to the instructions. The block is marked as reference data and cites node ids the model can quote back.

from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from vinc.integrations.agent_framework import VincContextProvider

vinc = VincContextProvider()              # reads VINC_API_KEY; use a vinc_ro_ key
agent = Agent(
    client=OpenAIChatClient(),
    instructions="You review design changes.",
    context_providers=[vinc],
)
await agent.run("Why are our colour tokens stored as OKLCH?")
ArgumentDefaultMeaning
clientmade from the arguments belowAn AsyncVincClient to share.
api_key, base_url, spaceVINC_API_KEY, Vinc, personalUsed when no client is passed. For a team's graph pass space="<team id>".
search_fallbackTrueWiden with a search when nothing matched or several nodes tied (a second call).
max_chars, max_excerptspackage default, 3Size of the block.
min_query_chars3Shorter messages are not looked up.
expose_toolsFalseAlso give the model the two read-only tools below.
prefetch_role, prefetch_max_charsnone, 24000Read this role and its evidence before every run (below).
source_id"vinc"The name the provider's instructions and tools are added under in the run context.
tool_calls_per_run4With expose_tools=True, how many times one run may call the two tools together; None never caps.
prefetch_pinnoneA contract hash from role(). A run whose role has changed stops before the model is called (below). Needs prefetch_role.

In context mode, if Vinc is unreachable or the daily limit is used up, the run goes ahead without the block. With prefetch_role the run stops instead. A wrong key or space raises.

Let the model look things up

VincContextProvider(expose_tools=True) adds two read-only tools, vinc_brief and vinc_search. To build them yourself, use create_vinc_tools(client).

Record after a person approves

The provider never stores the conversation: its after_run does nothing. When a person has approved the result, record it yourself with a write key.

import os
from vinc import AsyncVincClient
from vinc.integrations.agent_framework import record_episode

writer = AsyncVincClient(api_key=os.environ["VINC_WRITE_KEY"])   # vinc_sk_
await record_episode(
    writer,
    "Fixed contrast on the dark secondary button",
    summary="text-secondary now passes 4.5:1 on the dark surface.",
    about=["decision:tokens-are-oklch"],
)

Retry the record step

From vincs 0.1.4, record_episode and write_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 VincContextProvider. Before every run 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. The model gets the evidence without having to decide to look it up.

from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from vinc import AsyncVincClient
from vinc.integrations.agent_framework import VincContextProvider

reader = AsyncVincClient()                             # vinc_ro_ key
agent = Agent(
    client=OpenAIChatClient(),
    instructions="Review the proposed change.",
    context_providers=[VincContextProvider(
        reader, prefetch_role="concept:agent-reviewer-judgement", expose_tools=True,
        prefetch_max_chars=120000,
    )],
)

Prefetch reads the complete role contract and each required document by its declared document id on every run, without a cache. 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), before_run 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. The prefetch helper takes the same value as pin. It comes with vincs 0.1.5.

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

Check the final answer against the role

VincOutputGateMiddleware checks the final response outside the model, against the role's gates, never_checks and person_only. On block or a pending approval it raises OutputGateStopped. It calls no model and writes nothing.

from vinc import OutputGate
from vinc.integrations.agent_framework import VincOutputGateMiddleware

role = await reader.role("concept:agent-reviewer")     # read fresh for this run
gate = OutputGate(role["props"])
agent = Agent(
    client=OpenAIChatClient(),
    instructions="Review the proposed change.",
    middleware=[VincOutputGateMiddleware(gate)],
)
await agent.run("Review the proposed change.")         # run(), not a stream

Ask a person before a person-only step

Have the model step answer with a vinc.RoleOutput and connect a VincPersonOnlyExecutor after it. When the answer asks for a person-only action, the workflow emits the framework's own request_info event. Send the person's decision back as the response; the model is not called again.

from agent_framework import WorkflowBuilder
from vinc import ApprovalDecision
from vinc.integrations.agent_framework import VincPersonOnlyExecutor

approval = VincPersonOnlyExecutor(gate, role_id=role["id"], run_id="review-1")
workflow = WorkflowBuilder(start_executor=model_executor).add_edge(model_executor, approval).build()

decision = ApprovalDecision(request_id=event.data.request_id, approved=True)
await workflow.run(responses={event.request_id: decision})

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. Configure workflow checkpoints if a pending approval must survive a restart.

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.agent_framework import PackageProvider, write_package

handoff = await 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",
)
receiver = Agent(
    client=OpenAIChatClient(),
    instructions="Review the prior conclusion and cite available evidence.",
    context_providers=[PackageProvider(reader, handoff["package_id"], space=handoff["space"])],
)

Name the space; the evidence must already exist there. PackageProvider rereads the package before every run and checks each evidence id with the reader's own key, so deleted or unreadable evidence is marked as not citable. Bind one provider per handoff.

Structured review, one retry

run_review(agent, messages) asks 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. Each attempt runs with tools turned off.

from vinc.integrations.agent_framework import UnparsedReview, run_review

try:
    review = await run_review(reviewer_agent, "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.agent_framework. The functions take an AsyncVincClient first and are awaited.

HelperParametersWhat it does
VincContextProviderthe table aboveContext before each run, and optionally the read tools. aclose() closes a client it made itself.
PackageProviderclient, package_id, space, source_id, max_charsA handoff package before each run. space is required; source_id defaults to "vinc-package".
create_vinc_toolsclient, max_chars, capThe read-only tools vinc_brief and vinc_search. Pass cap=ToolCallCap() from vinc.sdk.tools to cap them at 4 calls; make a new cap for each run.
prefetchclient, role_id, task, space, max_chars, pinThe role's required reading and task evidence. Raises like role mode.
record_episodeclient, title, summary, about, date, domain, tags, space, episode_id, create_only, operation_idOne episode. Needs a write key.
write_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.
run_reviewagent, messages, native_structured, optionsA ReviewOutput, with one retry.
VincOutputGateMiddlewaregate, evidenceChecks the final response of a run that does not stream.
VincPersonOnlyExecutorgate, role_id, run_id, evidence, idAn approval executor that emits request_info.

Over MCP instead

Agent Framework can also reach the Vinc MCP server directly. Keep the tool list to reads, and set load_prompts=False if you do not use role prompts.

import os
from agent_framework import MCPStreamableHTTPTool

vinc = MCPStreamableHTTPTool(
    name="vinc", url="https://mcp.vincs.io/mcp",
    static_headers={"Authorization": "Bearer " + os.environ["VINC_API_KEY"]},
    allowed_tools=["vinc_brief", "vinc_search"],
    load_prompts=False,
)