Tarski/ docsJoin the waitlist

Effects & reconciliation

How Tarski touches the world — declared capabilities, admission identity, retry policies, the effect lifecycle, work queues, and explicit reconciliation.

Effects are downstream of semantic evaluation, always:

proposal.* → decision.approved.* → intent.* → effect request → attempt → receipt observation

Approved decisions derive intents; effect authority admits requests; the runtime executes declared capabilities; and results return only as receipt observations that re-enter the lineage as evidence. Guest code cannot mutate facts, append observations directly, or use undeclared ambient I/O.

Capabilities

Capability Purpose
http.fetch Declared outbound HTTP
llm.complete Explicit model calls for LLM-assisted agents
blob.put / blob.get Content-addressed large payload storage
timer.schedule A future timer observation
log.dev Developer diagnostics only — never canonical state

Capabilities are declared in tarski.toml and bound to intent relations. Admission identity is the lineage plus the intent relation name plus declared identity key values — which is what makes retries and crash recovery resolvable.

If a live binding’s credential reference cannot be resolved, the effect blocks with typed http.credential.unresolved before the network is opened — no receipt is appended, and nothing silently substitutes a deterministic provider.

Retries

Idempotent effects may auto-retry when the runtime classifies retry as safe. Non-idempotent or uncertain attempts enter reconciliation instead — policy never overrides the runtime’s safety classification.

Retry policies are declared per intent binding: fixed or exponential backoff, initial and maximum delays, optional deterministic jitter, and exhaustion bounds (max attempts and/or elapsed time) with on_exhausted = "failed" or manual reconciliation. Resolved policies are canonicalized and digested at admission — later manifest edits can never rewrite the policy an old decision ran under.

The effect lifecycle family

The runtime populates a reserved relation family your rules can read (never declare): effect.queued, effect.in_flight, effect.succeeded, typed effect.failed, effect.blocked, effect.reconcile_required, effect.conflict, plus grouping views effect.open and effect.terminal.

Rules that derive an intent at head H can read that effect’s lifecycle evidence only at a later head — same-head feedback loops are structurally impossible.

Work queues

Work queues are operational dispatch control for already-admitted requests — “when may this start?”, never “may this happen?”. Queue policy (concurrency, partitions, ordering, lease timing, rate limits, saturation behavior) is frozen at admission with its own digest; operators adjust capacity by appending durable queue-control records, not by editing manifests. Inspect with:

tarski work-queue list
tarski work-queue inspect

Reconciliation

When an effect attempt is ambiguous — a crash mid-flight, an uncertain provider — the runtime never silently guesses. The attempt parks as reconcile_required, and an operator resolves it explicitly:

tarski reconcile inspect --session latest
tarski reconcile resolve <attempt-id> --outcome <succeeded|failed|retry>

The resolution appends as an observation, so the decision is part of the lineage — auditable, replayable, and visible in provenance like everything else. Manual retry keeps the original attempt counter and frozen policy metadata.

Typed HTTP receipts

Plugin-owned http.fetch bindings may declare a receipt_kind plus a request template, producing typed, schema-validated receipt observations (e.g. a product-page fetch result) instead of the generic effect.result. App-owned HTTP bindings emit generic effect.result receipts in V1. result_kind belongs to llm.complete only. See Plugins.

App-owned HTTP envelope, egress, and size limits

An app-owned binding may materialize the request from named fields of an object-shaped intent:

[capabilities]
http_clients = ["calendar_feed"]

[capabilities.intents."intent.calendar_feed_requested"]
capability = "http.fetch"
resource = "calendar_feed"
identity_keys = ["account_id", "feed_id"]

[capabilities.intents."intent.calendar_feed_requested".request]
method = "GET"
path = "/feeds/$feed_id"
query = { account = "$account_id" }
headers = { accept = "text/calendar" }

[resources.http.calendar_feed]
base_url = "https://calendar.example.com"
allowed_hosts = ["calendar.example.com"]
credential_ref = "CALENDAR_API_TOKEN"
tls = "https_only"

Template placeholders use $field_name and resolve only from scalar fields of the canonical intent object. method, exactly one of path or url, query, headers, body, and idempotency_key are supported. A JSON request body is an ordinary string template, so quote/escape it as TOML text; complex nested assembly is better done before the intent boundary and carried as one canonical string field.

If no request template is declared, the intent object itself must provide the concrete method, path or url, optional query/headers/body, and optional idempotency key.

App-owned results return as effect.result observations. The capture includes request id, attempt, correlation/admission identity, captured time, normalized request, response status/headers/body, and an egress audit block. Your mapper must explicitly map the fields the ontology needs; a non-2xx HTTP response is still a valid captured receipt. Pre-capture network/configuration failures stay typed effect lifecycle diagnostics and do not invent a result observation.

Egress policy is static:

  • With no allowed_hosts, only base_url’s host is admitted.
  • Entries may be exact hosts or suffix wildcards such as *.example.com; the suffix does not include the apex.
  • HTTPS is required by default, redirects and environment proxies are disabled, DNS results are pinned, and metadata/private/local destinations are denied unless the resource explicitly opts into private networking.
  • An owner-supplied URL may fill an intent field, but it cannot widen the resource allowlist. Use a platform-controlled host allowlist, a gateway/proxy resource, or a client-side fetch when tenants may enter arbitrary feed hosts.

Live HTTP response bodies are capped at 10 MiB before a capture is admitted. The selected mapper’s payload_limit can impose a smaller bound, and tenant HTTP-egress quotas can deny or pace traffic before that. For large durable content, use blob.put/blob.get and carry a BlobRef rather than expanding the mapper input ceiling.