Getting_Started
Getting Started
Everything in this walkthrough runs on your machine: your cluster, your bench simulator, your agents. No godon-dev credentials, no CI. When it's done you will have planted a coupling edge into a simulator with known ground truth, let two autonomous systemtenders discover and measure it through the shared substrate, read the measured response curves back out, and declared a wish that the engine planned, served, and held.
Budget 45-90 minutes, most of it waiting while the agents work.
A running godon stack. If you haven't set it up yet, see Setup. You'll need:
- Linux host with the godon helm chart deployed on a kind cluster (Setup)
kubectlconfigured with the kind kubeconfig- Docker (for the bench simulator and the godon CLI)
jqandcurl
The walkthrough uses the generic bench — a configurable synthetic coupling simulator with known ground truth. You plant the coupling (topology, strength, shape, noise), the engine discovers it, and you compare. Two autonomous systemtenders run against two coupled bench nodes; each one discovers the other through the shared substrate.
Step 1: Start the Bench Simulator
The bench runs as a single container exposing two virtual nodes with a planted coupling edge:
docker create --name bench-generic \
--user 0:0 -p 8090:8090 \
-e CONFIG_PATH=/topology.yaml -e PORT=8090 \
-e GENERIC_SEED=42 -e RUST_LOG=info \
ghcr.io/godon-dev/godon-bench-generic:<version>
# <version>: a released tag (see ghcr) — or drop ':<version>' to track the current release
# Plant the coupling: node-1 -> node-2, strength 0.7, saturation shape
cat > /tmp/topology.yaml <<'EOF'
nodes:
- id: node-1
params: 3
objectives: 2
base: saturation
param_lower: 0.0
param_upper: 100.0
weights: [[0.0, 1.0, 0.0], [0.3, 0.2, 0.5]]
- id: node-2
params: 3
objectives: 2
base: linear
param_lower: 0.0
param_upper: 100.0
weights: [[0.4, 0.3, 0.3], [0.2, 0.5, 0.3]]
edges:
- from: node-1
from_channel: 0
to: node-2
to_channel: 0
strength: 0.7
noise:
gaussian_sigma: 0.02
colored_sigma: 0.0
drift_rate: 0.0
EOF
docker cp /tmp/topology.yaml bench-generic:/topology.yaml
docker start bench-generic
docker network connect kind bench-generic 2>/dev/null || true
BENCH_IP=$(docker inspect bench-generic -f '{{.NetworkSettings.Networks.kind.IPAddress}}')
echo "Bench at: ${BENCH_IP}:8090"
curl -s http://127.0.0.1:8090/health | jq .
The /health response lists the nodes and edge count — your ground truth for what the engine should find.
The docker network connect kind line matters: it puts the bench on the same docker network as your kind nodes, so the systemtenders (which run inside the cluster) can reach it at BENCH_IP. Keep that variable — the configs in step 3 need it.
Step 2: Port-Forward the Godon API
export KUBECONFIG=<path to your kind kubeconfig>
API_POD=$(kubectl get pods -n godon -l component=api -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n godon --address 127.0.0.3 "${API_POD}" 9090:8080 &
sleep 3
curl -s http://127.0.0.3:9090/health
127.0.0.3 instead of 127.0.0.1 avoids colliding with anything already bound to localhost — the loopback range gives you free aliases.
Step 3: Create Targets and Systemtenders
Two kinds of object make an agent run: a target (the HTTP endpoint it tends) and a systemtender config (its parameter space, objectives, and characterization group). You write both yourself — four small files.
The targets just point at the bench nodes you started in step 1:
mkdir -p /tmp/godon-scenario && cd /tmp/godon-scenario
cat > target-node-1.yaml <<EOF
name: generic-node-1
targetType: http
spec:
url: "http://${BENCH_IP}:8090/node-1"
auth_type: none
EOF
cat > target-node-2.yaml <<EOF
name: generic-node-2
targetType: http
spec:
url: "http://${BENCH_IP}:8090/node-2"
auth_type: none
EOF
The systemtender config is where the experiment lives. Three parts matter: the parameter space (settings — three parameters on a 0-100 grid), the objectives vs observations split (objective_0 is judged, objective_1 is only watched — the detector reads both), and the interference_detection group — any two systemtenders sharing a group name begin coordinating: turn-taking, probing, measuring their coupling.
cat > systemtender-1.yml <<EOF
meta:
configVersion: "0.3"
strict_validation: false
systemtender:
type: bench_generic
settings: # parameter search space
generic:
param_0:
constraints:
- {step: 20.0, lower: 0.0, upper: 100.0}
param_1:
constraints:
- {step: 20.0, lower: 0.0, upper: 100.0}
param_2:
constraints:
- {step: 20.0, lower: 0.0, upper: 100.0}
interference_detection: # shared group name = these agents measure each other
group: bench-characterization
mode: active
convergence_threshold: 0.02
refinement_depth: 3
push_block_size: 10
pause_block_size: 10
cooldown_trials: 5
hold_params: {param_0: 50.0, param_1: 50.0, param_2: 50.0}
reconnaissance: # how this systemtender reads its target
type: http
http:
url: "http://${BENCH_IP}:8090/node-1"
objectives:
- name: objective_0
direction: maximize
reconnaissance:
service: http
path: /metrics/json
key: objective_0
samples: 3
aggregation: median
observations: # recorded but NOT judged — an extra detection channel
- name: objective_1
reconnaissance:
service: http
path: /metrics/json
key: objective_1
samples: 3
aggregation: median
rollback_strategies:
standard:
consecutive_failures: 10
target_state: previous
max_attempts: 3
on_failure: continue
timeout_seconds: 60
effectuation: # how this systemtender writes parameters
type: http
targetRefs: ["generic-node-1"]
endpoint_config:
method: POST
path: /apply
timeout_seconds: 30
EOF
# systemtender-2 is the same file pointed at node-2
sed 's/node-1/node-2/g' systemtender-1.yml > systemtender-2.yml
Note how the pieces connect: targetRefs: ["generic-node-1"] refers to the target by the name you gave it above, and the reconnaissance URL is the same node endpoint. Every knob is documented in the Configuration Guide — the defaults above are the validated characterization setup.
Now create the objects through the API, using the godon CLI (public image from ghcr; it converts the YAML into API calls). <version> is a released tag — drop it to track the current release:
CLI="docker run --rm --network host -v /tmp/godon-scenario:/work:ro -w /work ghcr.io/godon-dev/godon-cli:<version>"
API="--hostname 127.0.0.3 --port 9090 --insecure"
$CLI $API target create --file=target-node-1.yaml
$CLI $API target create --file=target-node-2.yaml
$CLI $API systemtender create --name=bench-char-1 --file=systemtender-1.yml
$CLI $API systemtender create --name=bench-char-2 --file=systemtender-2.yml
# Capture the systemtender UUIDs — the results in step 5 are keyed by them
SYSTEMTENDER_1_UUID=$(curl -s http://127.0.0.3:9090/systemtenders | jq -r '.[] | select(.name=="bench-char-1") | .id')
SYSTEMTENDER_2_UUID=$(curl -s http://127.0.0.3:9090/systemtenders | jq -r '.[] | select(.name=="bench-char-2") | .id')
echo "sender: ${SYSTEMTENDER_1_UUID}, receiver: ${SYSTEMTENDER_2_UUID}"
(The scenario library ships these same files pre-written for many topologies — useful once you know the shape. Typing them once teaches the shape.)
Step 4: What the Systemtenders Are Doing
Each systemtender is a walking agent. Between walks, they coordinate through lease-based turn-taking to characterize their coupling:
- One systemtender acquires the sender role, the other holds still (receiver).
- The sender runs an adaptive walk — pushing one parameter at a time through its levels, chosen from the measured map: the highest-priced unresolved gap next, until every dial's remaining ignorance is priced below its measurement bar.
- For every push, the causal service measures the receiver's objective shift between push and pause windows, with an uncertainty bar from the raw sample scatter.
- Re-measurements within bars blend (the point tightens); beyond bars relocate and flag drift.
- A parameter retires when it is converged and every remaining gap in its response curve is priced below the local measurement bar — remaining ignorance cheaper than one more probe.
The result is a response curve per (sender, parameter, channel) — the measured shape of the coupling, not just a yes/no edge.
Step 5: Watch Progress, Read the Results
Progress: check the systemtender states and give it time. First probe blocks typically appear within ~15 minutes; a complete carrier curve at these block sizes takes 30-75 minutes.
curl -s http://127.0.0.3:9090/systemtenders/${SYSTEMTENDER_1_UUID} | jq '{status, name}'
While the run is in progress (or after), query the causal service directly:
CAUSAL_POD=$(kubectl get pods -n godon -l component=godon-causal -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n godon --address 127.0.0.3 "${CAUSAL_POD}" 9091:8091 &
sleep 3
# Detection verdict for one direction (sender -> receiver)
curl -s http://127.0.0.3:9091/detect/${SYSTEMTENDER_1_UUID}/${SYSTEMTENDER_2_UUID} | jq .
# All measured response curves — the coupling map
curl -s http://127.0.0.3:9091/curves | jq '.curves[] | {sender_id, receiver_id, param, channel, points: .state.points}'
The /detect response reports per-channel rising/falling edges with confidence; /curves returns every measured (level, shift, bar) triple — level on the sender's parameter grid, measured shift on the receiver's objective, and the uncertainty bar from the raw samples.
For the planted topology above, expect: param_1 of the sender showing a saturation-shaped curve on the receiver's objective_0, param_0/param_2 flat (they carry no weight), and objective_1 flat (the edge feeds channel 0 only). Curves keyed the other direction (node-2 as sender) stay flat — nothing is planted that way.
The same map through the API — the /connectome family, no causal port-forward needed (the API relays). Small examples, one concern each:
Query it. The live connectome — every node and characterized edge currently believed in:
curl -s http://127.0.0.3:9090/connectome | jq '{built_at, nodes: (.nodes | length)}'
Interpret it. The edges are the measured coupling. Each detected edge says: this sender's push shifts that receiver's channel, with what confidence, over what noise floor:
curl -s http://127.0.0.3:9090/connectome | \
jq '.edges[] | select(.detected) | {sender_id, receiver_id, channel, confidence, noise_floor}'
For the planted topology, expect exactly one detected edge — your node-1 → node-2 coupling — and its confidence against the noise floor tells you how firmly the engine believes it.
Export it. The artifact is the map persisted at last build — complete at build time, the citable object (Connectome):
curl -s http://127.0.0.3:9090/connectome/artifact > /tmp/connectome.json
jq '{built_at, edges: (.edges | length), curves: (.curves | length)}' /tmp/connectome.json
Ask it — one hop. If the sender pushes, what shifts? Reads never touch the system:
cat > /tmp/predict.json <<EOF
{"sender_id": "${SYSTEMTENDER_1_UUID}", "impulse_scale": 1.0}
EOF
curl -s -X POST http://127.0.0.3:9090/connectome/predict \
-H 'content-type: application/json' \
-d @/tmp/predict.json | jq .
One prediction per measured path from the sender; an ask outside the measured range is refused rather than extrapolated.
Ask it — chains. The same ask walks whole measured chains — /connectome/predict/multihop with the same body — composing the measured pieces to the far end.
Scout a wish from it. The prediction names the shift a push causes — wish for values the map says are reachable: read the current value (step 6 does), predict where it can go (above), and cut the band around a value the map can reach. Two honest caveats: predictions are linearized from the measured curves, and the map scouts — the judge settles. The wish's verdicts come from live readings, never from the prediction.
Two more reads in the family: /connectome/impact/{systemtender_id} — what a systemtender's probing has moved across the map — and /connectome/causes/{systemtender_id} — the measured causes upstream of its nodes.
This is the same measured map the wish planner compiles against in step 6: the map that predicts is the map that steers.
Step 6: Declare a Wish
The map is measured — now use it. A wish declares a chosen state; it is validated at declaration, the measured map plans the input setting, a systemtender serves it, and the judge reads the outcome against its band.
First read the receiver's current value — the wish keeps what you have, it does not invent a target:
curl -s http://${BENCH_IP}:8090/node-2/metrics/json | jq .
Cut the band around the reading you just saw (example assumes objective_0 reads 0.42 — substitute yours; a width of ±0.05 is generous at this noise level). Want a different value than today's? Scout it first — step 5's predict shows where the reading can go; bands the map can reach are the ones that land.
cat > /tmp/wish.json <<EOF
{
"type": "steer",
"claims": [
{ "outcome": "${SYSTEMTENDER_2_UUID}/objective_0",
"band": { "lo": 0.37, "hi": 0.47, "target": 0.42 } }
]
}
EOF
WISH_ID=$(curl -s -X POST http://127.0.0.3:9090/wishes \
-H 'content-type: application/json' \
-d @/tmp/wish.json | jq -r .id)
echo "wish: ${WISH_ID}"
Watch it live:
curl -s http://127.0.0.3:9090/wishes | jq '.[] | {id, state}'
curl -s http://127.0.0.3:9090/wishes/${WISH_ID} | jq '{state, events: [.events[] | {type, at}]}'
The wish moves declared → planned → acted: the door validates the grammar before any planning, the map compiles the claim into an input setting (the measured map decides which input serves the claim), and the assigned systemtender applies it and holds. Every judge verdict is the conjunction — each claim read against its band, with its evidence in the event history. A wish that cannot be kept is refused by name, never silently degraded.
One timing rule: the map needs measured curves on the path before it can plan — declaring before they exist is refused with the reason. If you ran ahead, wait for step 5's curves and declare again.
When done, close it — the serving systemtender releases its setting back to neutral:
curl -s -X POST http://127.0.0.3:9090/wishes/${WISH_ID}/close | jq '{state}'
The payload grammar — type stated, claims under terms, limits, budget — is documented in the Configuration Guide; the record's lifecycle in Wish.
Step 7: Verify with No Coupling
Run the same walkthrough with the edge strength set to 0.0 in topology.yaml (or a topology without edges). Expect flat curves everywhere and no detections — the honest blank. Zero false positives across the sweep is a core validated property of the method.
Step 8: Clean Up
# Close the wish if you have not already — it binds nothing once closed
curl -s -X POST http://127.0.0.3:9090/wishes/${WISH_ID}/close > /dev/null
# Purge the systemtenders — their measured curves and observation rows are removed with them
$CLI $API systemtender purge --force --id=${SYSTEMTENDER_1_UUID}
$CLI $API systemtender purge --force --id=${SYSTEMTENDER_2_UUID}
# Remove the targets (ids from: curl -s http://127.0.0.3:9090/targets)
curl -s -X DELETE http://127.0.0.3:9090/targets/<target-id>
docker rm -f bench-generic
Understanding the Protocol
The method is the impulse protocol: non-destructive perturbation within guardrail bounds, ABA block design (push → pause → compare), and CFAR detection with adaptive thresholds — the same statistical family as radar and sonar. Receiver exploration noise — the dominant interference signal blocker — is eliminated by the receiver holding still during measurement rather than by stronger statistics.
That is the perception half. The action half is the wish you declared in step 6: the same measured map, planning an input setting and holding a chosen state against drift — Wish.
Full methodology and validation: Interference Detection.
Varying the Planted Reality
The topology file is the experiment design:
| Knob | What it changes |
|---|---|
strength |
Edge coupling strength (0.1 floor to 0.9, validated sweep) |
base |
Per-node shape: linear, threshold, saturation, polynomial |
gaussian_sigma |
Measurement noise (0.02 validated; boundary characterized 0.05-0.10) |
edges |
Any DAG: chains, stars, multi-agent groups |
Next Steps
- Interference Detection — the methodology and its validation boundaries
- Wish — the declaration surface this walkthrough touched, in full
- Architecture — the causal service and the components around it
- Bench Scenarios — the scenario library