Agent frameworks
Python SDK
The Python SDK in vincs calls the Vinc HTTP API. Use it directly, or add the LangGraph or Microsoft Agent Framework adapter from the same package.
prefetch (role mode). prefetch raises when Vinc cannot be read; get_context (context mode) returns None and the model call goes ahead without context. The table below compares them.Install and connect
Requires Python 3.11 or later. This package also installs the vinc account command. See CLI installation for terminal use and the local runtime requirements.
This page describes vincs 0.1.5, which carries vinc-client 0.1.12. A feature that came after vincs 0.1.2 names the release it came with. The separate vinc-client package has the same methods under from vinc_client import VincClient.
pip install vincs
from vinc import VincClient
vinc = VincClient() # reads VINC_API_KEY
team = VincClient(space="<team id>") # a team's graph instead of your personal one
| Argument | Default | Meaning |
|---|---|---|
api_key | VINC_API_KEY | A member key starting with vinc_ro_ (read) or vinc_sk_ (read and write). |
space | personal | personal or a team id. |
base_url | VINC_API_URL, then https://mcp.vincs.io | Must be https; plain http only for localhost. |
timeout | 30.0 | Seconds per request. |
transport | None | An httpx transport, for tests or a proxy. |
prefetch_concurrency | 8 | How many reads prefetch sends at once, from 1 to 8. With 1 they go one after another. |
retry_for | 90.0 | Seconds to keep retrying while the hub is briefly down; 0 turns retries off. See When the hub is briefly down. |
Use it as a context manager, or call close() when you are done. AsyncVincClient has the same methods, awaited, and closes with aclose().
Read-only and write keys
| Key | What it can call |
|---|---|
vinc_ro_ | Every read on this page, including operation_status and role_check. |
vinc_sk_ | The reads, plus ingest, record_episode, write_package, delete, delete_edge, merge_nodes and doc_delete. |
With a read-only key, every write method raises VincAuthError with code read_only_key before anything is sent. A dry_run preview of a delete or merge counts as a write, because the server ranks it with the change it previews. vinc.read_only tells you whether the client holds a read-only key.
Context for one model call
block = vinc.get_context("Why are our colour tokens stored as OKLCH?")
if block:
system_prompt += "\n\n" + block
get_context returns a text block ready to append to a system prompt, or None when nothing matched. The block is fenced and marked as reference data, because text in the graph can be written by anyone with access to it. It cites node ids the model can quote back.
| Argument | Default | Meaning |
|---|---|---|
search_fallback | False | When the lookup finds nothing or several nodes tie, widen with a search (a second call). |
max_chars | package default | Cap on the block's length. |
max_excerpts | 3 | Excerpts per node. |
min_query_chars | 3 | Shorter queries return None without a call. |
If Vinc is unreachable or the daily limit is used up, it logs a warning and returns None, so your model call goes ahead. A key or space problem raises.
Reading
| Method | Parameters | What it returns |
|---|---|---|
whoami | · | The key's owner, plan and the teams it can reach. |
brief | query, max_excerpts, historical, mode, as_of, known_at | One node that answers the question (or an id), with excerpts and links. status is resolved, choose or none. |
search | query, limit, cursor, detail, historical, mode, as_of, known_at | Text and meaning search, paged with cursor. detail is summary or full. |
recall | domain, tags, limit, cursor | The episode timeline, newest first. |
node | node_id | One node by id. Raises VincNotFound if it does not exist. |
edges | node or nodes, type, dir, limit | A node's stored links with their direction and evidence. Pass exactly one of node and nodes. From vincs 0.1.4. |
document | doc_id | A document's exact text with its content hash. |
role | role_id, pin | A role's whole contract with its rev and hash. See Roles and their evidence. |
role_check | role_id, output, person_only_requests, self_authored | The hub applies the role's checks to an output and answers pass, block or interrupt. A pass grants no permission. From vincs 0.1.4. |
operation_status | operation_id | The receipt of an earlier write. See Retry a write. From vincs 0.1.3. |
get_context | query, search_fallback, max_chars, max_excerpts, min_query_chars | A block for one model call, or None. See above. |
prefetch, prefetch_with_contract | role_id, task, max_chars, pin | The block a model reads before it answers as the role; the second also returns the contract it read. See Roles and their evidence. |
get_package_context | package_id, space, max_chars | A handoff package, checked again with this key. See Hand a result to another agent. |
Every method also takes space= to use another space for that one call. brief and search take mode (general, current or historical), as_of and known_at to scope the answer in time; historical=True is the older way to include expired evidence.
Writing, on purpose
Nothing in the package writes because a conversation happened. Write with a vinc_sk_ key, after a person approved the result.
writer = VincClient(api_key=os.environ["VINC_WRITE_KEY"])
writer.record_episode(
"Fixed contrast on the dark secondary button",
summary="text-secondary now passes 4.5:1 on the dark surface.",
about=["decision:tokens-are-oklch"],
)
record_episode reads every about id first, so a typo raises VincNotFound instead of creating an empty node. It also takes date, domain, tags and episode_id, and from vincs 0.1.4 create_only and operation_id, as write_package does.
ingest(nodes, edges) writes any nodes and links, merged by id. Properties you send replace the stored ones whole: read first and send the merged result, or, from vincs 0.1.3, put the node's expected_rev and a props_patch (a JSON Merge Patch) on it. If someone wrote in between, the whole write is refused. If part of the write was skipped or warned about, it raises VincPartialWrite; e.result holds what was stored and the warnings.
| Write method | Parameters | What it does |
|---|---|---|
ingest | nodes, edges, create_only, operation_id, review_claims | Writes nodes and links, merged by id. With review_claims=True it also compares Record and Decision claims with the ones you saved. |
record_episode | title, summary, about, date, domain, tags, episode_id, create_only, operation_id | One episode about nodes that already exist. |
write_package | question, conclusion, claims, produced_by, run_id, space, title, package_id, create_only, operation_id | One handoff package Record. space is required. |
delete | node_id, dry_run, preview_digest, operation_id | Deletes a node and its links. |
delete_edge | from_id, to_id, type, dry_run, preview_digest, operation_id | Deletes one link. |
merge_nodes | keep_id, drop_id, dry_run, preview_digest, operation_id | Folds drop_id into keep_id. |
doc_delete | doc_id, dry_run, preview_digest, operation_id | Deletes one document. |
When the hub is briefly down
The hosted hub can stop answering for a moment, for example while a new version is deployed. From vincs 0.1.4 the client retries those cases, so they read as a slower call rather than an error.
- Retried: HTTP 502, 503 and 504, a dropped connection and a timeout. Anything else, including 401, 404, 409, 429 and 500, raises at once.
- Reads and previews (
dry_run=True) are retried. A write is retried only when it carries anoperation_id, because without one the server could apply it twice. - The client waits as long as
Retry-Aftersays when the hub sends it, and otherwise waits longer after each try, up to 8 seconds. Once the next try would start more thanretry_forseconds (90 by default) after the first, it stops and raises the last error.retry_for=0turns retries off. get_contextandprefetchnever retry: they run inside a model call, so an outage makes them skip the context or stop at once. For your own code inside a model call,without_retries()returns the same client with retries off.
Retry a write
A write whose response was lost may or may not have landed. Give each logical write a fresh lowercase UUID as operation_id, and reuse it only to retry that write. ingest takes it from vincs 0.1.3; record_episode, write_package and the delete and merge calls from vincs 0.1.4.
import uuid
from vinc import VincUnavailable
op = str(uuid.uuid4()) # one id per logical write
try:
writer.record_episode("Release checklist approved", about=["decision:release-plan"],
episode_id="episode:release-checklist-approved", operation_id=op)
except VincUnavailable:
receipt = writer.operation_status(op) # decide from the receipt, then retry with the same op
- An identical retry with the same
operation_idreturns the stored result without applying again. Changed arguments under the same id conflict. operation_status(operation_id)reads the receipt.acceptedandunknowndo not prove the write did not happen;committedmeans the graph transaction committed. A replayed result is a historical summary, so read the targets for their current state.- Receipts belong to the owner, are kept until the owner's account is deleted, and are capped at 5,000 per owner. A read-only key can read them.
create_only=Truerefuses ids that already exist, before any change. Without it, writes keep merging by id. A write with anoperation_idis retried while the hub is briefly down; a write without one never is.
Delete and merge with a preview
delete, delete_edge, merge_nodes and doc_delete cannot be undone, so call each twice. With dry_run=True nothing changes and you get the complete preview with a preview_digest. Show it to a person, then apply with that digest. These calls come with vincs 0.1.4.
preview = writer.merge_nodes("concept:colour-tokens", "concept:color-tokens", dry_run=True)
# show the preview to a person, then apply exactly what they saw
writer.merge_nodes("concept:colour-tokens", "concept:color-tokens",
preview_digest=preview["preview_digest"], operation_id=str(uuid.uuid4()))
If the affected state changed after the preview, the apply call is refused instead of acting on something nobody saw. A preview that would be too large, or that touches a target the key cannot reach, is refused as well. Both calls need a write key; an operation_id on the apply call makes it safe to retry.
Roles and their evidence
role(role_id) reads a role's stored contract as it is now. prefetch(role_id, task) builds the block a model should see before it answers: the whole contract, the full text of each document the role says to read first, and Vinc's material on the task, with node ids the model can cite.
role = vinc.role("concept:agent-reviewer-judgement") # id, props and the roles it works with
block = vinc.prefetch(
"concept:agent-reviewer-judgement", "Review the proposed change.",
max_chars=120000,
)
Prefetch reads the complete role contract and each required document by its declared document id on every call, 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. The combined 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.
role() also returns the contract's rev and hash. To keep an agent on the contract you reviewed, pass that hash as pin to role, prefetch or prefetch_with_contract. When the role has changed, the call raises VincRoleChanged before any document is read, with the current id, rev and hash in details. Moving the pin to the new hash is your decision. The pin covers the role contract, not the text of the documents it lists. Over REST this is expected_hash on GET /v1/roles/{id}, which answers 409 role_changed. It comes with vincs 0.1.5.
pin = vinc.role("concept:agent-reviewer-judgement")["hash"] # after you reviewed it
block = vinc.prefetch(
"concept:agent-reviewer-judgement", "Review the proposed change.",
max_chars=120000, pin=pin,
)
Role mode or context mode
In the framework adapters, role mode calls prefetch and context mode calls get_context.
| Mode | When Vinc is unreachable or the daily limit is used up | Hub calls per call | Tools the role disallows | Pinning |
|---|---|---|---|---|
Role mode (prefetch) | Raises, with no retry. Any failed read raises the same way. | 1 for the role, 2 per required document (the document and its node), 1 brief and 1 search. | prefetch_with_contract also returns the contract, and RoleToolPolicy applies its disallowed_tools to tools with exactly those names. The framework adapters do this for you. | Optional, from vincs 0.1.5: with pin set to a contract hash from role(), a changed role raises VincRoleChanged before any document is read. Without it each call reads the role as it is now. |
Context mode (get_context) | Logs a warning and returns None, so the model call goes ahead without context. A key or space problem raises. | 1 brief, plus a search with search_fallback=True when it ties or finds nothing. None for a query shorter than min_query_chars. | Not applied: there is no role. | Not applicable: each call reads the graph as it is. |
The SDK keeps nothing between calls; the framework adapters decide how often to call. prefetch_with_contract and RoleToolPolicy come with vincs 0.1.4.
Hand a result to another agent
Write one package Record with the question, the conclusion and each claim's evidence, and give the next agent its id. Passing only citation ids loses the conclusion.
handoff = writer.write_package( # vinc_sk_ key
"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 = vinc.get_package_context(handoff["package_id"], space=handoff["space"]) # next agent
The space must be named, and the evidence must already exist in it. Each read checks every evidence id again with the reader's key, so deleted or unreadable evidence is marked as not citable. Source text is never copied into the package. Use a new package id for each handoff.
Check an answer against a role
OutputGate checks a final answer against the role's gates, never_checks and person_only, outside the model. evaluate returns pass, block or interrupt; require_pass raises OutputGateStopped on the last two.
from vinc import OutputGate
gate = OutputGate(role["props"])
result = gate.evaluate(final_answer)
print(result.verdict) # pass, block or interrupt
A claim that something ran needs a ToolProof built from a real tool call your host saw in this run; never build one from the model's output. A prose-only contract or an unknown check blocks. A pass releases the answer and nothing more: it does not run, permit or undo a tool. RoleOutput, prepare_person_only and resolve_person_only turn a person-only request into an approval bound to that exact draft; the framework packages wire them into their own approval steps.
Validate a review
parse_review checks a reviewer's JSON against ReviewOutput: a verdict of ACCEPT or RETURN, findings with citations, and person-only requests. RETURN needs findings and ACCEPT needs none; malformed JSON or extra fields raise. A valid shape does not prove the citations exist or that anyone approved.
Errors
| Class | When |
|---|---|
VincError | The base class. Every error below carries code, message, status, request_id, details and retry_after. |
VincAuthError | The key is missing, malformed, refused, or read-only for a write. Fix the configuration. |
VincNotFound | The node, document or space does not exist for this key. |
VincInvalidArgument | The request was malformed or refused: a topic limit, a create_only id that exists, an expected_rev or preview_digest that no longer matches, or an operation_id reused with other arguments. |
VincRoleChanged | A pinned role has changed. No contract comes back; details holds the current id, rev and hash. It is a kind of VincInvalidArgument. |
VincQuotaExceeded | The daily limit is used up. retry_after is in seconds. |
VincUnavailable | Network or server failure, raised once the retries above have run out. For a write, check details["write_may_have_applied"] and operation_status before retrying. |
VincPartialWrite | Part of a write was skipped or warned about. result holds what the server said. |
Other public names
Everything below is importable from vinc, or from vinc_client in the separate package.
| Name | Parameters | What it is |
|---|---|---|
format_context | brief, search, max_chars, limit | The block get_context returns, built from a brief and the search that widened it. |
format_answer | brief, search, max_chars, limit | The same block without the instruction to end with a Sources: line. |
format_brief, format_search | brief or result, max_chars, and limit for a search | One brief or one search as a block. |
fence | body | Wraps text in a backtick fence longer than any backtick run inside it. |
BOUNDARY, SOURCES_LINE | · | The sentence that marks a block as reference data, and the instruction to end every answer with a Sources: line. |
sources_line | text | The ids on an answer's final Sources: line: [] when it names none, None when the line is missing. |
build_episode_fragment | title, summary, about, date, domain, tags, episode_id | The fragment record_episode sends. |
build_package_fragment | question, conclusion, claims, produced_by, run_id, title, package_id | The fragment write_package sends. |
format_package | node, space, readable_ids, evidence_nodes, max_chars | A package Record as a block, with each claim marked citable or not. |
check_package_handoff | node, space, gate, readable_ids, repositories, evidence_nodes, proofs, max_chars | Checks an existing package with the producer's gate before you pass it on, keeping its conclusion and claims word for word. |
OutputGate | props, extra_gates, self_authored | evaluate takes answer, proofs, person_only_requests, phase, manifest, resolved_sources, repositories and returns a GateResult (verdict, failed_checks, person_only_actions, evidence_sentences); require_pass raises OutputGateStopped instead. See Check an answer against a role. |
ToolProof | action, target, call_id, success, claim | A tool result your host saw in this run, passed to evaluate. |
preserve_required_route | base, override | Refuses a route override that drops or skips a required gate runner step. |
RoleOutput | text, person_only_requests | The answer shape a model returns when it may ask for a person-only action. |
prepare_person_only | output, gate, role_id, run_id, evidence | Returns the checked output, or an ApprovalRequest (request_id, role_id, run_id, output, actions) bound to that draft. |
resolve_person_only | request, decision | Resumes the saved request with an ApprovalDecision (request_id, approved) and returns an ApprovalResult (status, output, actions). |
ReviewOutput, Finding, PersonOnlyRequest | verdict, findings, person_only_requests; a finding has what, cite, quote, rule; a request has action, reason | See Validate a review. |
parse_review, UnparsedReview | value | Checks a reviewer's JSON against ReviewOutput. The framework review helpers raise UnparsedReview (status is UNPARSED) after their one retry. |
RolePrefetch | block, contract | What prefetch_with_contract returns. |
RoleToolPolicy | role_id, disallowed | from_contract(contract) reads a contract's disallowed_tools; allows(name), allowed(tools), unmatched(tools), warn_unmatched(tools) and refusal(name) apply them by exact name. |