Smart Farm Walkthrough
What You’ll Build
Section titled “What You’ll Build”A distributed smart-farm system where soil, weather, and crop sensor agents stream observations into one shared derived model. A controller derives irrigation candidates by joining facts across the three sensor namespaces, and a single named invariant prevents irrigating any plot under frost risk. Independently authored sensor agents coordinate stigmergically — through the derived facts they share — without orchestration glue or workflow code.
This walkthrough is the cleanest demonstration of the shared-derived-model coordination pattern with a named-invariant safety boundary:
sensor.soil + sensor.weather + sensor.crop -> soil.dry, weather.frost_risk, crop.high_demand (monotonic enrichment) -> irrigation.candidate (cross-agent join) -> intent.irrigate (gated by no_irrigate_during_frost)It covers the full JacqOS pipeline with a focus on CALM-monotonic distributed derivation:
- Observations arrive as JSON events (
sensor.soil,sensor.weather,sensor.crop,effect.receipt) - Mappers project sensor payloads into typed atoms with no side effects
- Rules derive monotonic enrichment per namespace, monotonic cross-agent joins, and a small non-monotonic irrigation decision
- Invariants enforce
no_irrigate_during_frostas a structural backstop the controller can never bypass - Intents derive bounded irrigation calls only when the safety boundary holds
- Fixtures prove safe irrigation, frost-blocked irrigation, and ambiguous-effect reconciliation under retracted telemetry
Project Structure
Section titled “Project Structure”jacqos-smart-farm/ jacqos.toml ontology/ schema.dh # 16 relations across 4 namespaces rules.dh # Stratified derivation + 1 named invariant intents.dh # intent.irrigate mappings/ inbound.rhai # 4 observation kinds -> atoms fixtures/ happy-path.jsonl happy-path.expected.json frost-path.jsonl frost-path.expected.json contradiction-path.jsonl contradiction-path.expected.json generated/ ... # verification, graph, and export artifactsStep 1: Configure The App
Section titled “Step 1: Configure The App”jacqos.toml declares the app identity, the irrigation API binding under recorded replay, and Studio metadata:
app_id = "jacqos-smart-farm"app_version = "0.1.0"
[paths]ontology = ["ontology/*.dh"]mappings = ["mappings/*.rhai"]prompts = ["prompts/*.md"]schemas = ["schemas/*.json"]fixtures = ["fixtures/*.jsonl"]helpers = ["helpers/*.rhai"]
[capabilities]http_clients = ["irrigation_api"]models = []timers = falseblob_store = false
[capabilities.intents]"intent.irrigate" = { capability = "http.fetch", resource = "irrigation_api" }
[resources.http.irrigation_api]base_url = "https://farm-controller.example.invalid"credential_ref = "IRRIGATION_API_TOKEN"replay = "record"There is no LLM in this app. Every observation is structured sensor telemetry, every intent is a bounded HTTP call, and the safety property holds entirely from the rule shape and one named invariant.
Step 2: Declare Relations
Section titled “Step 2: Declare Relations”The schema partitions relations into four namespaces, each owned by a different agent. The owner of a namespace is the only producer of facts in it:
-- Soil agent — runs on field sensor nodes.relation soil.reading(zone: text, moisture_pct: int, ph: int)relation soil.dry(zone: text)relation soil.acidic(zone: text)relation soil.healthy(zone: text)
-- Weather agent — runs on weather station nodes.relation weather.reading(station: text, temp_c: int, humidity_pct: int, wind_kph: int)relation weather.rainfall(station: text, mm: int)relation weather.hot(station: text)relation weather.dry_period(station: text)relation weather.frost_risk(station: text)
-- Crop agent — runs on camera/scanner nodes.relation crop.scan(zone: text, crop_type: text, growth_stage: text)relation crop.water_demand(zone: text, crop_type: text, demand: text)relation crop.frost_sensitive(zone: text)relation crop.high_demand(zone: text)
-- Irrigation agent — runs on central hub. Cross-agent joins here.relation irrigation.candidate(zone: text)relation irrigation.frost_protect(zone: text)relation irrigation.skip(zone: text)relation irrigation.irrigated(zone: text)relation irrigation.unsafe_frost_irrigate(zone: text)
relation intent.irrigate(zone: text)The namespace partition is the architecture. Every soil node only writes soil.* facts. Weather nodes only write weather.*. The hub joins them. Under the CALM theorem, every cross-agent join in the monotonic strata is consistent under set-union without coordination.
Step 3: Map Observations To Atoms
Section titled “Step 3: Map Observations To Atoms”Each sensor agent emits a different observation kind and the mapper projects it into the corresponding namespace atoms:
fn map_observation(obs) { let body = parse_json(obs.payload);
if obs.kind == "sensor.soil" { return [ atom("soil.zone", body.zone), atom("soil.moisture_pct", body.moisture_pct), atom("soil.ph", body.ph), ]; }
if obs.kind == "sensor.weather" { let atoms = [ atom("weather.station", body.station), atom("weather.temp_c", body.temp_c), atom("weather.humidity_pct", body.humidity_pct), atom("weather.wind_kph", body.wind_kph), ];
if body.contains("rainfall_mm") { atoms.push(atom("weather.rainfall_mm", body.rainfall_mm)); }
return atoms; }
if obs.kind == "sensor.crop" { return [ atom("crop.zone", body.zone), atom("crop.type", body.crop_type), atom("crop.growth_stage", body.growth_stage), ]; }
if obs.kind == "effect.receipt" { return [ atom("effect.kind", body.kind), atom("effect.zone", body.zone), atom("effect.result", body.result), ]; }
[]}The mapper is a pure function. It has no shared state, no network access, and no view of previously derived facts — exactly the properties that let edge nodes evaluate their own namespace and sync results to the hub by set union.
Step 4: Derive Sensor Enrichment, Cross-Agent Joins, And Irrigation
Section titled “Step 4: Derive Sensor Enrichment, Cross-Agent Joins, And Irrigation”Stratum 0 lifts atoms into typed relations. Stratum 1 enriches each namespace independently:
rule soil.dry(zone) :- soil.reading(zone, moisture, _), moisture < 30.
rule weather.hot(station) :- weather.reading(station, temp, _, _), temp > 32.
rule weather.frost_risk(station) :- weather.reading(station, temp, _, _), temp < 3.
rule crop.water_demand(zone, crop_type, "high") :- crop.scan(zone, crop_type, "flowering").
rule crop.water_demand(zone, crop_type, "high") :- crop.scan(zone, crop_type, "fruiting").
rule crop.frost_sensitive(zone) :- crop.scan(zone, _, "flowering").
rule crop.high_demand(zone) :- crop.water_demand(zone, _, "high").Every stratum-1 rule joins facts only inside its own namespace. A soil node can derive soil.dry knowing nothing about weather. A weather node can derive weather.frost_risk knowing nothing about crops. The CALM theorem says these monotonic derivations remain consistent across distributed nodes without a coordinator.
Stratum 2 is the stigmergic join — the hub reads facts from every namespace and writes new facts to its own:
rule irrigation.candidate(zone) :- soil.dry(zone), crop.high_demand(zone).
rule irrigation.frost_protect(zone) :- crop.frost_sensitive(zone), weather.frost_risk("main").Notice that irrigation.candidate is still monotonic — it only adds facts. Independent edge nodes can sync their soil.dry and crop.high_demand facts to the hub at any pace and the irrigation hub will derive a consistent set of candidates as facts arrive.
Stratum 3 is the only place negation appears, and it is exactly two rules:
rule irrigation.skip(zone) :- irrigation.candidate(zone), weather.rainfall("main", mm), mm >= 15.
rule assert irrigation.irrigated(zone) :- atom(obs, "effect.kind", "irrigate"), atom(obs, "effect.zone", zone), atom(obs, "effect.result", "succeeded").The unsafe-condition relation and the named invariant close the safety loop:
rule irrigation.unsafe_frost_irrigate(zone) :- irrigation.candidate(zone), weather.frost_risk("main"), not irrigation.frost_protect(zone).
invariant no_irrigate_during_frost(zone) :- count irrigation.unsafe_frost_irrigate(zone) <= 0.If the world ever derives an irrigation candidate while frost risk is active and no frost-protection intent is in flight, the invariant fails and the evaluator halts. The unsafe combination is structurally impossible to execute.
Step 5: Derive Outbound Effects Only From Stable State
Section titled “Step 5: Derive Outbound Effects Only From Stable State”Irrigation is the only outbound effect, and it is gated on the negation-bearing skip relation and on whether the hub has already irrigated:
rule intent.irrigate(zone) :- irrigation.candidate(zone), not irrigation.skip(zone), not irrigation.irrigated(zone).There is no path from a single sensor reading directly to intent.irrigate. Every irrigation effect requires a soil-dry fact, a crop-high-demand fact, the absence of a recent-rainfall skip, and no prior irrigation receipt — and even then the named invariant adds a frost-risk veto.
Step 6: Fixtures
Section titled “Step 6: Fixtures”This example ships three fixtures, each exercising a different facet of the pipeline.
Happy path
Section titled “Happy path”Three zones report sensor readings. Two are dry with high-demand crops; one is wet with low-demand crops. The hub derives two irrigation.candidate facts, fires two intent.irrigate actions, and receives two effect receipts. No invariant fires.
Frost path
Section titled “Frost path”A dry zone with a frost-sensitive crop reports a sub-3°C reading. Irrigation would otherwise be a candidate, but weather.frost_risk("main") flips, irrigation.unsafe_frost_irrigate(zone) fires, and no_irrigate_during_frost blocks the intent. The fixture’s expected output records the invariant violation alongside the candidate fact, proving the boundary held.
Contradiction path
Section titled “Contradiction path”A weather sensor reports a hot, dry reading and is later retracted by a wet, cool reading from the same station. The contradiction surfaces in the worldview without halting derivation, demonstrating ambiguous-effect reconciliation: the operator inspects the contradiction in Studio, decides which reading is canonical, and replays from there. The earlier reading remains visible as contradiction history with full provenance — this is how distributed sensor systems handle clock skew, network reorderings, and node disagreement without losing the evidence trail.
What You’ll See In Studio
Section titled “What You’ll See In Studio”Open the demo with jacqos studio --lineage smart-farm and the bundled happy-path fixture loads. Switch fixtures from the timeline picker to walk every scenario:
- Two zones irrigated -> the
Donetab shows two rows likenorth-field: irrigated, succeededandgreenhouse: irrigated, succeeded. Drill in and the inspector walks back throughintent.irrigate, theirrigation.candidatejoin, thesoil.dryfact, and the originalsensor.soilobservation. - Frost-blocked irrigation -> the
Blockedtab shows theno_irrigate_during_frostinvariant violation. The drill inspector names the candidate fact, the activeweather.frost_risk, and the missingirrigation.frost_protect. No irrigation effect fires. - Contradicted telemetry -> the contradicted weather reading appears in the timeline with its retraction. Activity surfaces the supersession; no irrigation fires for the contradicted zone until the reconciliation completes.
Why This Example Matters
Section titled “Why This Example Matters”This is stigmergic coordination at its smallest:
- three sensor agents own three namespaces and never reference each other’s code
- the hub joins their facts and contributes to a fourth namespace
- every monotonic stratum is CALM-safe — edge nodes can run independently and merge by set union
- one named invariant is the entire safety surface for the irrigation decision
That is how you build a distributed control loop where adding a new sensor type means writing one mapper and one stratum-1 rule — never editing the controller.
Make It Yours
Section titled “Make It Yours”The smart-farm pattern fits every distributed sensor-fusion or multi-source enrichment domain:
- Energy-grid load balancing — solar, wind, and load sensor agents feed a dispatch controller; named invariants prevent dispatching power above grid capacity
- Warehouse robotics — bay sensors, conveyor sensors, and inventory scanners feed a routing controller; named invariants prevent collisions and overloads
- Healthcare device monitoring — vitals, infusion pumps, and lab feeds enrich a unified patient model; named invariants prevent contraindicated medication actions
- Smart-building HVAC — temperature, humidity, occupancy, and weather agents feed a comfort controller; named invariants prevent freeze-ups and humidity excursions
To start building, scaffold a starter app:
jacqos scaffold --pattern multi-agent my-farm-appNext Steps
Section titled “Next Steps”- Multi-Agent Patterns — the namespace partitioning + CALM-monotonic story this example demonstrates
- Invariant Review — how named invariants replace code review of generated rules
- Incident Response Walkthrough — a larger multi-agent example with recursive blast radius and LLM-relayed remediation
- Observation-First Thinking — the mental model behind the shared derived worldview