nvoken Python SDK
An Invocation is one durable agent turn. The host supplies agent_key,
optional tenant_key, session_key, and idempotency_key; instructions,
model, and tools travel with the turn as an agent_definition, either inline or
referenced by a reusable agent_definition_id.
The package has three deliberate levels:
Agentis the ordinary workflow facade:text,run,invoke,stream, and locally serialized bound Sessions.ClientandInvocationHandleexpose durable operations, transcript drains, provider-key lifecycle, iterators, configurable waits, and resumable streams.nvoken_generatedis the complete generated Runtime transport and raw escape hatch.
python -m pip install nvoken
NVOKEN_BASE_URL=http://localhost:8080 NVOKEN_API_KEY=... \
python examples/quickstart.py
The async facade provides durable handles, replay-safe retries, typed errors,
cursor iterators, resumable SSE, composed result reads (result,
list_messages, output_text), and callback verification. Session-scoped
messages use Client.list_session_messages.
Resolve the identity-only Agent anchor without admitting work:
agents = await client.list_agent_identities(agent_key="support")
identity = await client.get_agent_identity(agents.items[0].id)
The identity contains only its nvoken ID, host-owned key, and creation time. Instructions, models, tools, and provider keys remain per Invocation.
Opt into the fixed guarded public-web reader with fetch_tool():
from nvoken import AgentOptions, Model, fetch_tool
options = AgentOptions(
agent_key="research",
model=Model(provider="anthropic", id="claude-sonnet-5"),
tools=(fetch_tool(),),
)
The Runtime accepts only {"name":"nvoken_fetch","mode":"builtin"}. It owns
public-address checks, up to five guarded redirects, one transient retry,
HTML-to-Markdown conversion, and the ten-second and 64 KiB limits. Run
python examples/fetch.py to summarize NVOKEN_FETCH_URL.
Use an Agent for the common path:
agent = client.agent(AgentOptions(
agent_key="support",
instructions="Help with billing questions.",
model=Model(provider="anthropic", id="claude-sonnet-5"),
))
print(await agent.text("Why was I charged twice?"))
continued = agent.session(session_key="customer-123")
print(await continued.text("What should I do next?"))
A bound Session serializes admission only within that local binding. The
Runtime remains authoritative across processes and rejects a second
nonterminal turn. Agent operations dispatch configured host-tool handlers.
If a waiting call has no handler, the Agent cancels before raising
MissingToolHandlerError by default; set
InvocationOptions(leave_waiting_on_missing_handler=True) only when another
worker deliberately owns it. NoOutputTextError.result_kind distinguishes
structured, tool-only, and empty completions.
For an intentional replace/regenerate action, use a new idempotency key and the typed option:
handle = await agent.invoke(
"Try that answer again.",
options=InvocationOptions(
idempotency_key="customer-123:regenerate-2",
if_active="supersede",
session_key="customer-123",
),
)
Omission or "reject" preserves the default conflict response. Low-level
callers set the same policy on InvokeRequest.if_active.
if_active="interrupt" is the keep-the-work variant: the active Invocation
stops at its next execution seam and settles completed with stop_reason
"interrupted", so the replacement turn builds on what it already produced.
await handle.interrupt() asks for the same graceful stop without admitting a
replacement, and Invocation.stop_reason names why any turn ended.
A turn can also stop without ending: "incomplete" means the Runtime enforced
a budget at a seam, with stop_reason naming which one. It is terminal — the
wait helpers stop there — and its work is kept, so treat it as an unfinished
answer rather than an error. SessionMessage.phase says which assistant
message was the reply: "final_answer" on the one that ended a completed
turn, "commentary" on everything else, so an incomplete turn has none.
InvocationOptions(timeout=...) is one overall local deadline. Cancelling the
calling task still raises native asyncio.CancelledError; it does not imply a
durable Runtime cancellation. Call handle.cancel() when that is intended.
Recovery reads accept a status union, and a known Invocation can stream only durable frames:
page = await client.list_invocations(
status=["queued", "running", "waiting"],
)
async for event in handle.events(deltas=False):
...
Equivalent status sets share cursor identity regardless of input order.
Session get/list models expose typed nullable usage, computed from durable
Invocation usage as a convenience estimate rather than a billing ledger.
Install restart-stable compaction on a new or existing Session:
from nvoken import ContextCompaction, SessionOptions
request = InvokeRequest(
agent_key="support",
session_key="support:123",
session_options=SessionOptions(
compaction=ContextCompaction(trigger_tokens="auto"),
),
input="hello",
agent_definition=AgentDefinition(
model=Model(provider="anthropic", id="claude-sonnet-5"),
),
)
Use an integer trigger and optional same-provider model for explicit policy.
A Session without a policy accepts late opt-in; once installed, the policy is
immutable. Supplied options on an existing Session must equal stored values or
admission returns session_options_conflict.
Summary usage appears in Session usage rather than Invocation usage. Read
applied and fell-through diagnostics with
client.list_session_compactions(session_id).
InvocationOptions.metadata correlates a turn with your own records from the
Agent binding. It is part of the admitted input, so it is immutable and material
to idempotency: a replay carrying different metadata conflicts rather than
updating it. That is why it is per-call rather than an AgentOptions default.
Pass a stored or one-turn provider key directly through
InvokeRequest:
request = InvokeRequest(
agent_key="support",
input="hello",
agent_definition=AgentDefinition(
model=Model(provider="openai", id="gpt-test"),
),
provider_keys=(
ProviderKeySelection(
provider="openai",
source="caller_ephemeral",
api_key=provider_key,
),
),
)
Stored sources are app_byok, tenant_byok, and platform and do not
accept an api_key. Client.stream_session(session_id, reducer, consume)
follows the Session until its task is cancelled; a terminal turn does not end
the Session stream. For catch-up reads, use get_transcript_page when
checkpointing each page or drain_transcript to consume one fixed cut.
Discover models through the same async facade:
catalog = await client.list_models(provider="openai")
selected = await client.get_model(
Model(provider="openai", id=catalog.items[0].id)
)
print(selected.cataloged, selected.pricing.status)
The list is curated discovery metadata, not proof of provider-account access. Exact inspection also accepts uncataloged IDs.
Set an explicit portable temperature on the request or Agent:
from nvoken import AgentDefinition, InvokeRequest, Model, Sampling
request = InvokeRequest(
agent_key="support",
input="hello",
agent_definition=AgentDefinition(
model=Model(provider="anthropic", id="claude-haiku-4-5"),
sampling=Sampling(temperature=0),
),
)
Omit sampling to preserve the provider default. Check
selected.controls.sampling.temperature first; missing controls are unknown,
and unsupported or unknown selections fail before durable admission. The
portable range is [0,1]. top_p and stop sequences are intentionally absent;
limits.max_output_tokens is the output guardrail.
Reasoning is typed and fail closed:
from nvoken import AgentDefinition, InvokeRequest, Model, Reasoning
request = InvokeRequest(
agent_key="support",
input="hello",
agent_definition=AgentDefinition(
model=Model(provider="anthropic", id="claude-opus-5"),
reasoning=Reasoning(effort="high"),
),
)
Check selected.controls.reasoning first. budget_tokens requires a larger
explicit limits.max_output_tokens. Omission preserves provider defaults;
unsupported values and combinations are rejected without aliasing. OpenAI
reasoning remains unavailable until its complete continuation representation
is durable.
Structured-output schema preflight
Client.invoke and Agent operations call
preflight_output_schema(schema) before transport when
InvokeRequest.agent_definition.output_schema is present. Rejection is an NvokenError with
code schema_preflight_failed; its safe details contain the portable issue
code, RFC 6901 path, and optional keyword. A successful local check means
eligible for admission. Generated APIs reached through client.raw() still
rely on the authoritative Runtime check.
Reuse an Agent Definition
Sending the definition inline is the ordinary path. Register it instead when many turns share one configuration and you would rather send a short ID:
resource = await client.create_agent_definition(AgentDefinition(
instructions="Help with billing questions.",
model=Model(provider="anthropic", id="claude-sonnet-5"),
), idempotency_key="support-definition-v1")
handle = await client.invoke(InvokeRequest(
agent_key="support",
input="Why was I charged twice?",
agent_definition_id=resource.id,
))
Creating a definition starts no turn and creates no Agent, Session, or message.
The resource has a stable ID and an increasing revision. Use
get_agent_definition() and update_agent_definition() to read and replace
it. An idempotency key makes create retries safe; equal content under another
key creates an independent resource.
Supply exactly one of agent_definition and agent_definition_id; the facade
rejects a request carrying both or neither before it reaches the network.
AgentOptions supports the same choice; host tool handlers remain local when a
reusable resource supplies the declarations.
Record changing application state
Keep instructions static. Product state that changes between turns — a board
snapshot, customer facts, the current policy — belongs in context:
answer = await agent.text(
"Can I refund the duplicate charge?",
InvocationOptions(
session_key="ticket-483",
context=(
ContextItem(name="customer", tier="contextual", content="plan: pro"),
ContextItem(
name="refund-policy",
tier="operator",
content="Self-serve refunds cap at 50 USD",
),
),
),
)
A name is a stable identity. Send it once and nvoken records it as a leading
message the model reads as app-customer; omit that reserved prefix here.
Send the same name again only when its value changes — a byte-identical resend
is accepted but adds no message, so a stateless host may resend its whole
snapshot every turn and get the same transcript as a host that tracks changes.
Use contextual for conversation-adjacent facts and operator for policy or
other application-authoritative state. Context is Session history, not an Agent
Definition field: it never changes agent_definition_id, and later turns keep
sending it to the model even when you omit it. That is what keeps the prompt
prefix stable enough for provider caching, which rewriting the same state into
instructions would break on every turn.
The list is order-sensitive and part of idempotency, so a replay that reorders
or edits an item conflicts rather than updating it. A request accepts at most
eight items, 8 KiB per item, and 16 KiB in total; the SDK checks all three
before the request leaves the process. A Session may accumulate at most 16
distinct names, which only the service can check. Retire a name by superseding
it with a short current value such as "ticket: closed".
Remote MCP tools
Use the handwritten declaration for discovery and Invocation admission:
server = MCPServer(
name="support",
url="https://mcp.example.com/rpc",
allowed_tools=("lookup_order",),
timeouts=MCPTimeouts(discovery_seconds=10, call_seconds=30),
)
headers = {"Authorization": f"Bearer {mcp_token}"}
catalog = await client.list_mcp_tools(server, headers)
request = InvokeRequest(
agent_key="support",
input="hello",
agent_definition=AgentDefinition(
model=Model(provider="anthropic", id="claude-sonnet-5"),
mcp_servers=(server,),
),
mcp_server_headers=(MCPServerHeaders(name="support", headers=headers),),
)
The declaration carries no secrets. An Agent Definition may be reused across
turns, so authentication headers travel per Invocation in
mcp_server_headers, keyed to the server name. They are hidden from dataclass
representation, are one-Invocation secret material, and never appear in durable
Agent Definitions or public recovery surfaces.
Callback tools
A callback tool runs on an HTTPS endpoint nvoken posts to. Verify the signed
delivery with verify_callback, then answer with one of two replies.
callback_result(content, is_error=False) settles the ToolCall inline and the
turn resumes as soon as nvoken records the reply. acknowledge_callback()
returns 202 with no body instead: it accepts the delivery without settling the
call, for work that will outlive the App's callback reply deadline. Settle it
later with client.submit_tool_results, reusing the delivery's ToolCall ID.
Acknowledging trades away the fail-loud guarantee. nvoken marks an
unacknowledged delivery failed once its retries are exhausted, so the turn
always moves on. An acknowledged call instead waits under your responsibility,
bounded only by the Invocation's limits.waiting_timeout_seconds. Acknowledge
only when something durable will settle the call. Such a call appears in a
waiting Invocation's pending calls the same way a host call does; an Agent
skips the callback tools its own definition declares rather than dispatching
them locally.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nvoken-0.17.0.tar.gz.
File metadata
- Download URL: nvoken-0.17.0.tar.gz
- Upload date:
- Size: 326.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0039385e0ae0454ab6a4e35a154716a3fcd8a09ef1491dd5b2c4cf859856b624
|
|
| MD5 |
c3c12ba17a169bb68f4179626fe87740
|
|
| BLAKE2b-256 |
8b9065331e04598257f9cb7e3996002c9fd34f230197af10c9595be632f42cb7
|
Provenance
The following attestation bundles were made for nvoken-0.17.0.tar.gz:
Publisher:
release-pypi.yml on deepnoodle-ai/nvoken
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nvoken-0.17.0.tar.gz -
Subject digest:
0039385e0ae0454ab6a4e35a154716a3fcd8a09ef1491dd5b2c4cf859856b624 - Sigstore transparency entry: 2476760094
- Sigstore integration time:
-
Permalink:
deepnoodle-ai/nvoken@a05bb8d463d885ff6bc9aaf9633c472a01823e70 -
Branch / Tag:
refs/tags/pypi-v0.17.0 - Owner: https://github.com/deepnoodle-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@a05bb8d463d885ff6bc9aaf9633c472a01823e70 -
Trigger Event:
push
-
Statement type:
File details
Details for the file nvoken-0.17.0-py3-none-any.whl.
File metadata
- Download URL: nvoken-0.17.0-py3-none-any.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71d45025d2c349743f8228944ce63342667b1c32958e0e06283b2ecaff86f04a
|
|
| MD5 |
fc5d165690bc5015c9c00b4181f7a4f3
|
|
| BLAKE2b-256 |
a7af55544338d5fa2a49446e3e04bdf87d86397e0be9c3dc88e03c4024260175
|
Provenance
The following attestation bundles were made for nvoken-0.17.0-py3-none-any.whl:
Publisher:
release-pypi.yml on deepnoodle-ai/nvoken
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nvoken-0.17.0-py3-none-any.whl -
Subject digest:
71d45025d2c349743f8228944ce63342667b1c32958e0e06283b2ecaff86f04a - Sigstore transparency entry: 2476760589
- Sigstore integration time:
-
Permalink:
deepnoodle-ai/nvoken@a05bb8d463d885ff6bc9aaf9633c472a01823e70 -
Branch / Tag:
refs/tags/pypi-v0.17.0 - Owner: https://github.com/deepnoodle-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@a05bb8d463d885ff6bc9aaf9633c472a01823e70 -
Trigger Event:
push
-
Statement type: