Skip to content

Clients

Two client libraries, both in the repository, both with no dependencies. Each wraps the HTTP/JSON API and adds a Session that remembers what you read and fills in based_on for you. Client and Session never retry, merge or wait: they hand you the rejection. The Python run() loop built on top of them does re-read and re-decide, which is its job.

Python

Install. pip install ./client/python from a checkout. Not on PyPI yet. No runtime dependencies.

from kslate import Client, Session, Agent, Write, run, FENCE

Client(base_url, api_key=None, timeout=30.0, *, token_source=None, token_file=None, ssl_context=None). The wire calls. Give exactly one credential:

  • api_key — a fixed token
  • token_file — re-read on every request, which is what a projected ServiceAccount token needs
  • token_source — a callable

ssl_context is for a gateway serving its own certificate. Verification is never disabled.

MethodReturns
get(record, fields=None)Record with .record, .record_version, .fields (a dict in the wire shape)
put(record, field, value, expected_version=None, evidence=None, based_on=None, invocation=None)The put response dict
append(record, field, entry, evidence, based_on=None, invocation=None)The append response dict
claim(record, field, lease_seconds=None, renew=False)The claim response dict
release(record, field)Same as claim with lease_seconds=0
checkpoint_save(record, document)checkpoint_id, record_version
checkpoint_load(record, role=None)document, record_version, changed_fields
query(class_name, filters=None, limit=None, after=None)records, each in the get shape; next_after when more remain
history(record, field=None)Every history row, newest first
history_page(record, field=None, limit=None, before=None)One page plus next_before

Session(client). Wraps a Client and adds read tracking. It has get, put, append, claim, release, checkpoint_save, checkpoint_load, query and premises. History is on Client only. get remembers each field’s version. put and append send those versions as based_on unless you pass your own. Pass based_on=[] to send none. A premise may carry "kind": FENCE. put and append also take invocation. Safe to share between threads.

Agent(decide, reads=[], writes=[], build_checkpoint=None, invocation=None). Your agent as configuration. decide(fields) -> list[Write]. reads and writes document intent and are not enforced client-side. build_checkpoint(fields) -> dict, if given, is saved after each successful cycle in class-scoped mode. invocation, if given, is attached to every write as the writer-asserted invocation object.

Write(op, field, value=None, entry=None, evidence=None, expected_version=None, invocation=None). One write. op is "put" or "append". expected_version left None on a put is filled in from the version read. invocation on a write overrides the agent’s.

run(agent, client, record=None, max_attempts=5, backoff_seconds=0.1, *, class_name=None, filters=None, claim_field=None, lease_seconds=None, concurrency=1, poll_interval=0.5, max_records=None, stop_event=None, install_sigterm_handler=True). With record, one read-decide-write cycle, retried on STALE and STALE_PREMISE up to max_attempts when decide returned one write. max_attempts=1 calls decide exactly once and raises the rejection instead of re-deciding — what an agent that acts while it decides wants, since a second call would act again. The record must already exist: the cycle begins with a get, so run raises APIError with NOT_FOUND against a record nothing has written yet. With class_name, a loop that polls query, claims claim_field per record, runs a cycle, checkpoints, and sleeps poll_interval when idle. It renews the claim while decide runs and stops renewing before it writes. Every write names the claim’s version in based_on as a fence. A write after a lost lease is then refused with STALE_PREMISE, and the cycle returns False. Renewals are paced from the lease the gateway granted, lease_expires_at on the claim response. They are never slower than a third of lease_seconds when you pass one. Stops on stop_event or SIGTERM. See The agent loop.

process_one(agent, client, record, claim_field=None, ...). One claim-decide-write-checkpoint cycle for one record. Returns False if the claim was HELD or the lease was lost before the write.

Errors. StaleError, StalePremiseError, HeldError, WeakIdentityError, APIError. See Handling rejections for their fields.

Go

Install. go get kslate.dev/kslate/client/go. In the repository it is client/go. Standard library only.

Constructors.

New(baseURL, apiKey string) *ClientBearer token fixed
NewWithTokenSource(baseURL string, token func() (string, error)) *ClientToken read per request
FileTokenSource(path string) func() (string, error)For a projected ServiceAccount token
(*Client).WithHTTPClient(*http.Client) *ClientA custom CA (TLSClientConfig.RootCAs), a proxy, or your own timeouts
(*Client).WithTimeout(time.Duration) *ClientPer-request timeout (default 30 s)
(*Client).NewSession() *SessionRead-tracking session

Client methods, all taking a context.Context first.

MethodReturns
Get(ctx, record, fields...)*Record with Fields map[string]Field
Put(ctx, record, field, value, PutOptions{ExpectedVersion, Evidence, BasedOn, Invocation})*PutResult
Append(ctx, record, field, entry, evidence, basedOn)*AppendResult
AppendWith(ctx, record, field, entry, AppendOptions{Evidence, BasedOn, Invocation})*AppendResult
Claim(ctx, record, field, leaseSeconds *int64, renew bool)*ClaimResult
Release(ctx, record, field)*ClaimResult
CheckpointSave(ctx, record, document)*CheckpointSaveResult
CheckpointLoad(ctx, record, role)*CheckpointLoadResult
Query(ctx, class, filters, limit)*QueryResult (first page; NextAfter when more remain)
QueryPage(ctx, class, filters, limit, after)*QueryResult
History(ctx, record, field)[]HistoryRow
HistoryPage(ctx, record, field, limit, before)*HistoryPage
Class(ctx, name)*ClassSpec

Session. Get, Put, Append as above. Get remembers versions. Put with BasedOn == nil and Append with basedOn == nil send the remembered versions. Pass an empty non-nil slice to send none. Premises(record, exclude) returns what would be sent.

Types. Field has Value json.RawMessage, Version, Author, Evidence *string, UpdatedAt, Owner *string, LeaseExpiresAt *time.Time, BasedOn []Premise, Entries []Entry, IdentityStrength *string and Invocation. A Premise is {Field string; Version int64; Kind string}. Its Kind is empty or PremiseFence. Invocation is a json.RawMessage. Entry and HistoryRow carry IdentityStrength and Invocation too.

Errors. *StaleError, *StalePremiseError, *HeldError, *WeakIdentityError, *APIError, tested with errors.As. Fields are listed in Handling rejections.

There is no run() in Go. The same agent in Go shows the loop.

Choosing

Use the Python client for run() and for anything that runs as a script or a job. Use the Go client inside a Go service or a sidecar. Both speak the same API and get the same errors. For an agent whose code you cannot change, use the sidecar instead of either.

There is no TypeScript or JavaScript client. Call the HTTP API directly: TypeScript over HTTP is one working file that does it.