Includes unreleased SDK 0.3.0 APIs.
Choose an interface
| API | Behavior |
|---|---|
client.bind() | Check the current authorized target on every access by default |
client.snapshot() | Fix the current snapshot on entry |
client.snapshot("review-42") | Remember and reuse a snapshot by application binding ID |
client.snapshot(source=ref) | Fix a specific actual snapshot reference |
Use these two primary interfaces. bind(consistency="pinned") and bind(source=ref) remain lower-level compatibility options. client.task(id) is just an alias for snapshot(id), not another mode.
Fixed and dynamic policies
async with client.bind(consistency="dynamic") as skills:
await skills.refresh() # explicit reconciliation; raises on failure
async with skills.snapshot() as batch:
body = await batch.read_text("review/SKILL.md")
rules = await batch.read_text("review/references/rules.md")
original_ref = batch.ref
- Opt-in
pinned: one snapshot and filtered manifest for the binding. Future channel changes do not change it. - Default
dynamic: reads/discovery reconcile on access afterrefresh_interval(default 0 seconds: every access).refresh()forces reconciliation. No background daemon or SSE subscription. - An expected-version load forces dynamic reconciliation even before the interval expires. Snapshot or catalog mismatch raises
VersionMismatch; no silent substitution. - A fully verified candidate replaces the active view atomically. Each operation captures one view; different operations can span versions.
snapshot()captures one view for related operations, even while the enclosing dynamic session updates. - Refresh failure aborts that operation and populates
last_refresh_error; it never silently reports stale discovery as fresh. Later successful refresh clears the error.transition_countcounts successful view changes. - The client default can be overridden per binding. An explicit
source=SnapshotRefselects pinned mode when consistency is omitted; explicitly combining it with dynamic is rejected. Reuse a ref for an explicit retry, subject to current server grants/permissions. view.refin dynamic mode is a point-in-time observation. Use returnedSkillContent.reforbatch.reffor provenance.
Task/attempt IDs are optional correlation fields and auto-generated if absent. Duplicate active IDs are rejected within one client. There is no durable attempt store, automatic crash resume or cross-process exactly-once guarantee. Persist refs in your application's own storage for explicit pinned retries. A ref does not grant access and cannot bypass a new credential's permissions.
Named snapshot bindings
# Default: check the current authorized target on each access.
async with client.bind() as skills:
catalog = await skills.catalog()
# First entry pins the current snapshot; re-entry reuses it.
async with client.snapshot("review-42") as skills:
catalog = await skills.catalog()
# Explicitly replace the binding with the latest authorized snapshot.
# This new view stays fixed throughout this block, too.
async with client.snapshot("review-42", retry_policy="latest") as skills:
catalog = await skills.catalog()
client.forget_snapshot("review-42")
client.snapshot(id) is the primary fixed-view API. The string is an application binding ID, not a server snapshot UUID. client.task(id) is an alias only. Without an ID, client.snapshot() captures an anonymous fixed view. To select an actual snapshot reference, use client.snapshot(source=ref).
Bindings survive application exceptions and context exit, so retrying the same ID reuses the original snapshot by default. retry_policy="latest" replaces the recorded binding only after successful preparation; it does not enable dynamic changes inside the block. Current permissions are always checked. Denied or changed views fail instead of silently moving to latest.
The mapping lives only in this client instance. Reuse a long-lived client across requests; separate clients, processes and restarts do not share bindings. Persist SnapshotRef in application storage and pass source=ref to recover across restarts. Concurrent contexts with the same ID raise SnapshotBusy; share the already opened view for parallel reads. The default capacity is 1,024 IDs (max_snapshot_bindings); call forget_snapshot(id) after completing work. Active bindings cannot be forgotten. Forgetting removes only the local mapping, not server snapshots or grants. There is no implicit eviction or expiry that could silently change a retry's version.
Optional durable bindings
from skiva_sdk import SkivaClient, SQLiteBindingStore
store = SQLiteBindingStore("bindings.sqlite", namespace="my-app/review-profile")
async with SkivaClient.from_env(binding_store=store) as client:
async with client.snapshot("review-42") as skills:
catalog = await skills.catalog()
print(catalog.render())
# A later process using the same database/namespace recovers this binding.
# Explicit cleanup after the application no longer needs retries:
# client.forget_snapshot("review-42")
The default store is client-local memory. Opt into SQLiteBindingStore to recover IDs across instances, processes and restarts, always subject to online authorization. Use an application/Profile namespace; server URLs are separated automatically. Place the database in a protected local directory, not a network filesystem. It contains references, not credentials or Skill bodies.
Publication uses atomic compare-and-set: a competing change during preparation raises SnapshotBusy instead of silently overwriting it. One client rejects overlapping contexts with the same ID; separate clients/processes may use their already fixed views concurrently. This is not a distributed execution lock. latest and deletion never mutate existing views; coordinate cleanup after all relevant executions finish. Records never expire implicitly; capacity applies per store namespace. Custom stores implement the atomic get/put/delete methods of BindingStore; methods should be fast local metadata operations.
Manifest ETag caching is bounded to 16 entries and a total of max_manifest_bytes. Every use still contacts the server for authorization; outages and denials never fall back to cached authorization. Fixed snapshots are not offline mode.
Retry policy is separate from update policy
Version 0.2.0 changes the client default from pinned to dynamic. Existing applications that need fixed ordinary bindings must pass consistency="pinned". Retry behavior is independently configured with retry_policy="reuse_snapshot" (default) or "latest", on the client or on client.retry().
async with client.bind(task_id="review-42") as skills:
catalog = await skills.catalog()
# Run your application/agent with this session.
checkpoint = skills.retry_checkpoint
# Default: the checkpoint snapshot stays fixed for the whole retry.
async with client.retry(checkpoint) as retry:
print((await retry.catalog()).render())
# Opt in: resolve the current authorized target and follow the client policy.
async with client.retry(checkpoint, retry_policy="latest") as retry:
print((await retry.catalog()).render())
retry_checkpoint is an immutable RetryCheckpoint(task_id, attempt_id, snapshot) containing the session's last fully published view, not necessarily its initial view. It is available after the binding exits, including when application code raises. Capture and keep it before further updates if you want that specific view. A failed/cancelled refresh does not advance it. A dynamic attempt may have used several versions: one checkpoint does not reproduce its full sequence of reads or model history. For an exact returned content/batch version, construct a checkpoint with that SkillContent.ref or SnapshotView.ref and the session's IDs.
client.retry(checkpoint) keeps the task ID, generates a new attempt ID, revalidates current permissions and fixes the checkpoint snapshot throughout the retry. It never falls back to latest on denied access or a changed manifest. retry_policy="latest" resolves the current authorized Profile target (including server pins) and then uses the client's consistency policy, dynamic by default. Each retry can produce its own checkpoint for another retry. The checkpoint records no secrets and grants no access; applications can persist it with dataclasses.asdict and reconstruct its nested SnapshotRef when loading.
Reusing a task ID with bind() does not infer a retry. The SDK does not automatically rerun business actions or retry framework tools; wrap the retry boundary you own. For an application failure, read skills.retry_checkpoint in the exception handler after bind exits, then explicitly call client.retry. A failure before the initial binding is prepared has no usable checkpoint. Choosing latest reloads SDK content, but does not erase historical messages; decide whether to start a fresh model run/context in the application.