Effects and Intents
JacqOS agents interact with the outside world through intents and effects. Intents are derived declaratively from .dh rules. The shell executes them through declared capabilities and records every step as observations. This guide walks through the full lifecycle — from deriving an intent to handling crashes and reconciliation.
Deriving intents from rules
Section titled “Deriving intents from rules”An intent is any relation with the intent. prefix. You declare it in your schema and derive it in your rules just like any other fact, but the shell treats it specially: derived intents trigger effect execution.
Here’s how the appointment-booking example declares and derives intents:
ontology/schema.dh — declare intent relations:
relation intent.reserve_slot(request_id: text, slot_id: text)relation intent.send_confirmation(request_id: text, patient_email: text, slot_id: text)ontology/intents.dh — derive intents from stable state:
rule intent.reserve_slot(req, slot) :- booking_request(req, _, slot), slot_available(slot), not slot_hold_active(req, slot), not booking_terminal(req).
rule intent.send_confirmation(req, email, slot) :- confirmation_pending(req, email, slot), not confirmation_sent(req), not confirmation_failed(req, _), not booking_cancelled(req, _).Key points:
- Intents derive from stable facts, not raw observations. The evaluator reaches a fixed point before any intent fires.
- Guard conditions (like
not booking_terminal(req)) prevent re-deriving intents that have already been acted on. - The evaluator re-derives intents on every evaluation cycle. The shell deduplicates — an intent that was already admitted or completed won’t re-execute.
Declaring effect capabilities
Section titled “Declaring effect capabilities”Every intent must map to a declared capability in jacqos.toml. The shell refuses to execute intents that lack a capability binding — undeclared capability use is a hard load error.
jacqos.toml — capability declarations:
[capabilities]http_clients = ["clinic_api", "notify_api"]models = ["intake_triage"]timers = trueblob_store = true
[capabilities.intents]"intent.reserve_slot" = { capability = "http.fetch", resource = "clinic_api" }"intent.send_confirmation" = { capability = "http.fetch", resource = "notify_api" }
[resources.http.clinic_api]base_url = "https://clinic.example.invalid"credential_ref = "CLINIC_API_TOKEN"replay = "record"allowed_hosts = ["clinic.example.invalid"]tls = "https_only"
[resources.http.notify_api]base_url = "https://notify.example.invalid"credential_ref = "NOTIFY_API_TOKEN"replay = "record"allowed_hosts = ["notify.example.invalid"]tls = "https_only"http.fetch is not ambient network access. Before live dispatch, JacqOS checks
the resource allow-list, pins the validated DNS result into the transport
resolver, blocks metadata endpoints, blocks private/local networks unless the
resource explicitly opts in, ignores environment proxies, and refuses to follow
redirects. The receipt records the egress decision so you can audit where the
effect was allowed to go.
V1 effect capabilities
Section titled “V1 effect capabilities”| Capability | Purpose | Replay behavior |
|---|---|---|
http.fetch | Declared outbound HTTP | Request and response captured; replay-only mode uses matching captures |
llm.complete | Explicit model call | Full envelope captured; replay-only mode uses matching captures |
blob.put / blob.get | Large raw body storage | Observations carry stable blob handles |
timer.schedule | Request a future timer observation | Shell records scheduling, later appends timer-fired observation |
log.dev | Developer diagnostics only | Never canonical state |
Each intent binds to exactly one capability and one resource. The credential_ref field names an environment variable — actual secrets never appear in jacqos.toml or observation logs.
Different capabilities for different intents
Section titled “Different capabilities for different intents”The medical-intake example shows how a single app can mix HTTP and LLM capabilities:
[capabilities.intents]"intent.request_extraction" = { capability = "llm.complete", resource = "extraction_model", result_kind = "llm.extraction_result" }"intent.notify_clinician" = { capability = "http.fetch", resource = "notify_api" }The intent lifecycle
Section titled “The intent lifecycle”Every intent follows a strict lifecycle with durable state at each step:
Derived → Admitted → Executing → Completed ↘ (crash) → Reconcile Required1. Derived
Section titled “1. Derived”The evaluator reaches a fixed point and produces a set of intent.* facts. These are candidate intents — they express what the system wants to do based on current facts.
2. Admitted
Section titled “2. Admitted”The shell durably records each new intent before any external call. Admitted intents survive restarts. This is the commit point: once admitted, the shell is responsible for driving the intent to completion or flagging it for reconciliation.
3. Executing
Section titled “3. Executing”The shell dispatches the intent through its declared capability. An effect_started marker is written. The external call happens. The response is recorded as a new observation, closing the loop.
4. Completed
Section titled “4. Completed”The shell writes an effect_completed receipt. The new observation feeds back into the evaluator, potentially deriving new facts, retracting old ones, or deriving further intents.
Each step is an observation in the append-only log. You can trace the full lifecycle of any effect through provenance:
intent derived → intent admitted → effect started → effect completed → response observationThe observation-intent-effect cycle
Section titled “The observation-intent-effect cycle”The full loop looks like this:
- Observations arrive (user input, API responses, timer fires)
- Mappers extract atoms from observations
- Evaluator derives facts and intents to a fixed point
- Shell admits new intents, executes effects through declared capabilities
- Effect results append new observations
- Repeat until no new intents are derived
This cycle continues until the system reaches quiescence — a fixed point with no new intents to execute.
Crash recovery
Section titled “Crash recovery”JacqOS’s effect system is designed for crashes. Every state transition is durable, so the shell can always determine what happened and what to do next.
On restart, the shell inspects every admitted intent:
- No
effect_startedmarker: safe to execute from scratch. effect_completedreceipt exists: already done, no action needed.effect_startedwithout terminal receipt: this is the ambiguous case. The external call may or may not have succeeded.
Auto-retry vs. manual reconciliation
Section titled “Auto-retry vs. manual reconciliation”When the shell finds an effect_started marker without a terminal receipt, it classifies the attempt:
Safe auto-retry
Section titled “Safe auto-retry”The shell automatically retries when it can prove the request is safe to repeat:
- Read-only requests: GET calls that don’t mutate external state
- Idempotency key present: the resource contract guarantees exactly-once semantics
- Request-fingerprint contract: the external API confirms replay safety
Auto-retried effects append a new effect_started observation, preserving the full audit trail.
Manual reconciliation required
Section titled “Manual reconciliation required”When replay safety cannot be proven, the effect enters reconcile_required state. This is the default for any mutation where the shell can’t confirm the outcome. The system does not guess — it stops and asks a human.
Common scenarios that require reconciliation:
- POST request to an external API without an idempotency key
- Payment or state-changing call where the response was lost
- Any effect where partial execution could cause inconsistency
Inspecting and resolving reconciliation
Section titled “Inspecting and resolving reconciliation”Use the CLI to inspect and resolve effects stuck in reconcile_required:
Inspect pending reconciliations
Section titled “Inspect pending reconciliations”jacqos reconcile inspect --session latestThis shows every effect that needs human resolution, including:
- The original intent and its provenance
- The capability and resource involved
- The
effect_startedtimestamp - What the shell knows about the attempt
Resolve a reconciliation
Section titled “Resolve a reconciliation”After investigating the external system, resolve the attempt with one of three positional resolution values: succeeded, failed, or retry.
# The effect succeeded externally — record successjacqos reconcile resolve <attempt-id> succeeded
# The effect failed externally — record failure so the evaluator re-derivesjacqos reconcile resolve <attempt-id> failed
# Unknown — let the evaluator re-derive and the shell retryjacqos reconcile resolve <attempt-id> retryEvery resolution appends a manual.effect_reconciliation observation. The evaluator re-runs, deriving new facts based on the resolution. If the intent conditions still hold, a new intent may be derived and executed cleanly.
Replay and effects
Section titled “Replay and effects”During replay-only execution, the shell uses recorded provider captures instead of making live external calls. This means:
- Effects execute deterministically from recorded provider captures and observations.
- Replay-only resources never make external API calls.
replay = "record"captures provider envelopes;replay = "replay"requires a matching capture and refuses live dispatch.
This is how fixtures verify the full intent-effect cycle without touching real services.
Live effect authority
Section titled “Live effect authority”Live ingress uses the same lifecycle, but a live run also has to prove that
the evaluator and package are allowed to execute effects for the lineage.
JacqOS exposes that boundary through three effect authority modes:
| Mode | Behavior |
|---|---|
shadow | Evaluate, persist reports, and execute no effects. Use this for dry runs, replay parity, and subscriber-only live demos. |
prefer_committed_activation | Execute effects only when the loaded evaluator and package match the lineage’s committed activation. If they do not match, complete as shadow with a structured warning. |
require_committed_activation | Execute effects only when the loaded evaluator and package match. If no activation exists or the loaded package differs, return a typed authority error. |
Promote an activation when a lineage should be effect-authoritative:
jacqos activation promote --lineage live-demo --select-for-live --reason "reviewed fixtures"The serve API applies the same rule to POST /v1/lineages/{lineage_id}/run,
the chat adapter, and the webhook adapter. A chat message or webhook delivery
can append observations and evaluate in shadow, but it cannot force effects
for an uncommitted evaluator.
Worked example: booking with crash recovery
Section titled “Worked example: booking with crash recovery”Consider this sequence in the appointment-booking app:
- A
booking_requestobservation arrives for slotRS-2024-03 - The evaluator derives
intent.reserve_slot("REQ-1", "RS-2024-03") - The shell admits the intent and starts an HTTP call to
clinic_api - The process crashes mid-request
On restart:
- The shell finds
effect_startedwithout a terminal receipt for the reserve call http.fetchtoclinic_apiis a POST without an idempotency key — not safe to auto-retry- The effect enters
reconcile_required - The operator runs
jacqos reconcile inspect --session latestand sees the pending reservation - They check the clinic API and find the slot was successfully reserved
- They resolve:
jacqos reconcile resolve <attempt-id> succeeded - The resolution observation feeds back into the evaluator
confirmation_pendingis derived, leading tointent.send_confirmation- The confirmation email is sent normally
The entire chain — crash, reconciliation, and recovery — is visible in the observation log and traceable through provenance.
Best practices
Section titled “Best practices”- Keep intent rules narrow. Each intent should derive from the minimal set of facts that justify the action. Broad rules risk re-deriving intents in unexpected states.
- Use guard conditions. Always include negation guards that prevent re-firing after completion or failure (
not confirmation_sent(req),not booking_terminal(req)). - Declare all capabilities. The shell rejects undeclared capabilities at load time, not at runtime. This is a feature — it catches misconfiguration before any effects execute.
- Design for reconciliation. If your external API supports idempotency keys, use them. This turns manual reconciliation into safe auto-retry.
- Test the failure path. Ship contradiction-path fixtures that exercise failed effects, retries, and the full reconciliation cycle.
Going deeper
Section titled “Going deeper”- Debugging Workflow — when an effect fails or stalls, walk provenance back to the originating observation and pinpoint the rule that derived the intent.
- Debugging with Provenance — trace any derived fact or intent through the provenance graph.
- Crash Recovery — the concept behind reconciliation, including the durability guarantees the lifecycle relies on.
Next steps
Section titled “Next steps”- Fixtures and Invariants — verify intent-effect cycles with deterministic fixtures.
- Atoms, Facts, and Intents — the derivation pipeline that produces intents.
- CLI Reference — full surface for
reconcile,contradiction,audit, andreplay.