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, FENCEClient(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 tokentoken_file— re-read on every request, which is what a projected ServiceAccount token needstoken_source— a callable
ssl_context is for a gateway serving its own certificate. Verification
is never disabled.
| Method | Returns |
|---|---|
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) *Client | Bearer token fixed |
NewWithTokenSource(baseURL string, token func() (string, error)) *Client | Token read per request |
FileTokenSource(path string) func() (string, error) | For a projected ServiceAccount token |
(*Client).WithHTTPClient(*http.Client) *Client | A custom CA (TLSClientConfig.RootCAs), a proxy, or your own timeouts |
(*Client).WithTimeout(time.Duration) *Client | Per-request timeout (default 30 s) |
(*Client).NewSession() *Session | Read-tracking session |
Client methods, all taking a context.Context first.
| Method | Returns |
|---|---|
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.