Specify agent visual spatial derivation

This commit is contained in:
Meta Vens 2026-05-11 15:41:42 +01:00
commit 4a42e0cb01
2 changed files with 257 additions and 8 deletions

View file

@ -56,6 +56,164 @@ verticalLift, buoyancy, returnHomeStrength
traitActivations: map of canonical_state variable -> current_activation
```
## Normalization Rules
The renderer should convert loose surface strings into bounded scalars before
the shader sees them. First implementation rules:
```text
statusWeight("running") = 1.00
statusWeight("ready") = 0.72
statusWeight("needed") = 0.66
statusWeight("review") = 0.62
statusWeight("waiting") = 0.38
statusWeight("completed") = 0.34
statusWeight("blocked") = 0.18
statusWeight("unavailable") = 0.08
statusWeight(other) = 0.22
activity =
saturate(statusWeight(status) + min(jobCount, 3) * 0.08)
blocked =
status in {"blocked", "unavailable"} or blockingReason exists
completed =
status == "completed" or roleResult.status == "completed"
hasPatch =
roleResult.finding.statePatch exists
risk =
saturate(0.2 * count(finding.risks)
+ 0.16 * count(finding.evidenceGaps)
+ (blocked ? 0.25 : 0.0)
+ pressureWeight(pressure.level))
confidence =
finding.confidence if numeric
else 1.0 - saturate(0.5 * risk)
```
`pressureWeight` maps `unknown -> 0.15`, `low -> 0.1`, `medium -> 0.35`,
`high -> 0.65`, `critical -> 0.9`. Unknown is not zero; missing telemetry is
itself a small continuity smell.
## Coordinate Contract
All role placement and role-local SDF orientation uses Aquarium world space:
- `+X`: Grid right.
- `+Y`: Grid forward.
- `+Z`: Grid up.
- `gridCenter`: camera target on the XY plane.
- `selfAnchor`: `gridCenter` unless a future swarm-level anchor overrides it.
- `cameraFacingDir`: normalized horizontal vector from `gridCenter` toward the
camera. This is the "room/user side" direction.
- `cameraRightDir`: normalized horizontal camera right vector.
Each role has a stable home orbit slot around `selfAnchor`. These slots are
authoritative defaults for native Aquarium until Epiphany publishes explicit
world anchors:
| Organ | Role id | Angle | Ring | Home formula |
| --- | --- | ---: | ---: | --- |
| Self | `coordinator` | 0 deg | 0.00 | `selfAnchor` |
| Face | `face` | toward camera | 0.58 | `selfAnchor + cameraFacingDir * r` |
| Imagination | `imagination` | 135 deg | 0.78 | `selfAnchor + polar(135) * r` |
| Eyes | `research` | 35 deg | 0.86 | `selfAnchor + polar(35) * r` |
| Body | `modeling` | 215 deg | 0.66 | `selfAnchor + polar(215) * r` |
| Hands | `implementation` | 270 deg | 0.82 | `selfAnchor + polar(270) * r` |
| Soul | `verification` | 325 deg | 0.78 | `selfAnchor + polar(325) * r` |
| Life | `reorientation` | 80 deg | 0.62 | `selfAnchor + polar(80) * r` |
`r` is the current agent-orbit radius, independent of Grid visual radius. A
good first value is `min(gridRadius * 0.48, 22)`, clamped to `[10, 24]`.
`polar(angle)` means `float2(cos(angle), sin(angle))` in the XY plane. The Face
slot is camera-relative because Face is literally the public mouth; it should
prefer the room side. All other slots are world-stable so spatial memory does
not rotate every time the camera does.
## Derived Spatial Anchors
The renderer must derive named anchors before evaluating per-role motion. If a
source is missing, fall through to the next entry. No shader path gets to invent
its own private direction.
```text
roleAnchor(roleId) =
role home orbit slot
unless AgentVisualState.explicitWorldAnchor is present for that role
userAnchor =
selfAnchor + cameraFacingDir * agentOrbitRadius * 1.15
artifactAnchor(roleId) =
roleAnchor(roleId) + normalize(roleAnchor(roleId) - selfAnchor) * 3.5
+ cameraRightDir * 2.0
reviewAnchor =
roleAnchor("verification")
continuityAnchor =
roleAnchor("reorientation")
evidenceAnchor =
selected graph node world anchor, if a graph projection selected one
else artifactAnchor("research"), if Eyes has artifacts or evidence ids
else roleAnchor("research") + roleForward("research") * 3.0
targetAnchor =
roleAnchor(targetRole), if targetRole is a known role id
else evidenceAnchor, if recommendedAction == "graphQuery"
else continuityAnchor, if recommendedAction contains "reorient"
else reviewAnchor, if requiresReview
else roleAnchor(roleId) + roleForward(roleId) * 3.0
workAnchor =
active implementation job explicit world anchor, if present
else artifactAnchor("implementation"), if changed files or artifacts exist
else coordinator target role anchor, if targetRole is present
else reviewAnchor, if implementation is waiting on verification/review
else roleAnchor("implementation") + tangentClockwise("implementation") * 4.0
```
`tangentClockwise(role)` is the XY tangent around `selfAnchor` at that role's
home slot. It gives an idle directional bias without pretending there is a real
target.
`roleForward(role)` is `normalize(roleAnchor(role) - selfAnchor)` for every
orbiting role. For Self, it is `cameraFacingDir`. If a role is exactly at
`selfAnchor`, use `cameraFacingDir`.
`AgentVisualState.explicitWorldAnchor` is not part of the first native
implementation. It is reserved for a later CultNet field that publishes a graph
node, project label, or object anchor in Aquarium world coordinates. Until that
field exists, `roleAnchor` is always the home orbit slot and `workAnchor` uses
the fallback chain above.
State-derived vectors:
```text
homeVector = homeOrbitSlot - currentPosition
outwardVector = normalize(homeOrbitSlot - selfAnchor)
targetVector = targetAnchor - currentPosition
targetDirection = normalizeOr(targetVector.xy, outwardVector)
workDirection = normalizeOr(workAnchor.xy - handsPosition.xy,
tangentClockwise("implementation"))
evidenceDirection = normalizeOr(evidenceAnchor.xy - eyesPosition.xy,
roleForward("research"))
routeDirection = normalizeOr(roleAnchor(targetRole).xy - selfPosition.xy,
roleForward("coordinator"))
roleExpressionDirection =
normalizeOr(targetAnchor.xy - currentPosition.xy, roleForward(roleId))
```
`normalizeOr(value, fallback)` returns the fallback when the vector length is
below epsilon. This rule matters; near-zero direction vectors should not turn
into flickering NaNs with a nice color palette.
## SDF Rules
- Each organ gets a distinct SDF family, not only a color swap.
@ -92,6 +250,55 @@ Recommended controls:
- `verticalLift`: additional body-center lift derived from the local well pulse.
- `buoyancy`: role-specific damping/overshoot for vertical response.
Baseline placement integration:
```text
desiredOffset =
roleExpressionDirection * expressionAmount * wanderRadius
expressiveOffset =
dampedSpring(expressiveOffset, desiredOffset, returnHomeStrength, damping)
brushCenter =
homeOrbitSlot + expressiveOffset
gridHeight =
sampleSharedGridBrushes(brushCenter.xy)
bodyCenter =
float3(brushCenter.xy, gridHeight + 2 * bodyRadius + verticalLift)
```
Baseline role constants:
| Organ | Wander radius | Return strength | Buoyancy |
| --- | ---: | ---: | ---: |
| Self | 1.2 | 0.90 | 0.45 |
| Face | 3.2 | 0.62 | 0.85 |
| Imagination | 4.4 | 0.48 | 0.95 |
| Eyes | 2.4 | 0.78 | 0.52 |
| Body | 1.8 | 0.86 | 0.38 |
| Hands | 2.2 | 0.80 | 0.48 |
| Soul | 1.6 | 0.92 | 0.32 |
| Life | 2.0 | 0.84 | 0.55 |
The Grid brush uses the same `brushCenter` and `gravityWellPulse`. Vertical lift
is not an unrelated bob:
```text
gravityWellPulse =
heartbeatPulse * heartbeatLiftScale
+ actionPulse * actionLiftScale
- pressureSink * pressureSinkScale
verticalLift =
gravityWellPulse * buoyancy
```
The exact brush equation can change with the Grid implementation, but CPU
placement and shader placement must call the same equation or the agent will
visibly detach from its own well.
State mapping:
- Heartbeat primary state should create a small vertical pulse and brief outward
@ -138,7 +345,9 @@ Math:
Movement:
- Slow center pulse from heartbeat phase.
- Selected target role lights one gate and sends a traveling pulse along the
- `targetRole` resolves through `roleAnchor(targetRole)`. The active routing
gate is the torus arc whose tangent points closest to `routeDirection`.
- Selected target role lights that gate and sends a traveling pulse along the
matching arc.
- `requiresReview` adds a narrow dark seam around the core.
- `canAutoRun` sharpens and brightens the active gate.
@ -180,6 +389,8 @@ Movement:
- `hasBubble` opens the mouth and releases a small bead pulse.
- Drafts orbit close and dim; posts orbit farther and brighter.
- Speaking drift direction is `normalize(userAnchor - facePosition)`, capped by
Face's `wanderRadius`.
- Heartbeat primary state makes the lantern brighten from inside.
- Silence closes the mouth but keeps a slow listening breath.
@ -211,7 +422,8 @@ Math:
- Ribbon petals as torus-knot or swept-capsule loops around the seed.
- Smooth min unions for petal roots; sharp-ish material boundary near petal
tips.
- Harmonic count `k` driven by planning pressure and activity.
- Harmonic count `k = 4 + floor(activity * 3) + min(backlogCount, 3)`.
`amplitude = lerp(0.08, 0.28, activity)`.
- Material ids: opal seed, pearlescent petals, emissive idea sparks, cool
backlog shadow.
@ -223,6 +435,9 @@ Movement:
bloom.
- Uncertainty/evidence gaps add small branching petallets that fade rather than
becoming permanent complexity.
- Exploratory wander direction is the weighted sum of `outwardVector` and
`tangentClockwise("imagination")`; accepted plans set `expressionAmount`
toward zero so the flower returns home.
Surfaces:
@ -250,7 +465,7 @@ claim ownership of the answer.
Math:
- Lens from intersection of two spheres or a flattened ellipsoid with high
- Lens from intersection of two spheres with high
specular material.
- Aperture blades as repeated wedge/capsule SDFs around the front normal.
- Evidence sparks as small orbiting spheres or star-like octahedra gated by
@ -266,6 +481,8 @@ Movement:
- High confidence narrows aperture and steadies the lens.
- No evidence means no sparkle. The lens still watches, but it does not fake a
finding.
- Focus dart direction is `evidenceDirection`. `evidenceAnchor` is the
artifact anchor for Eyes until graph nodes publish explicit world anchors.
Surfaces:
@ -294,8 +511,12 @@ diagram shrine.
Math:
- Squashed ellipsoid core with smooth-union support lobes.
- Graph ribs as capsule networks over the surface, using a small fixed pattern
at first and future graph projections later.
- Graph ribs as capsule networks over the surface. First implementation uses
five deterministic ribs between fixed normalized surface points:
north-south, east-west, northwest-center-southeast,
northeast-center-southwest, and one equatorial ring segment. Future graph
projections may replace rib endpoints only when they publish explicit world
or role-local anchors.
- Node beads at rib intersections.
- Subtle displacement from low-frequency sine waves for breathing mass.
- Material ids: matte green body, glossy rib enamel, bright checkpoint nodes,
@ -308,6 +529,10 @@ Movement:
- Evidence gaps open small dark fissures between ribs.
- High load makes the mass sag; high confidence makes ribs align and glow
steadily.
- Connective wake direction points from Body toward any `frontierNodeIds`
world anchor when available, otherwise toward Self. This makes missing graph
anchors obvious: the wake falls back inward instead of pretending the graph
has a location.
Surfaces:
@ -334,7 +559,7 @@ then settles. No grand aura. Hands earns drama by producing diff.
Math:
- Capsule/ellipsoid core elongated along the current work direction.
- Capsule/ellipsoid core elongated along `workDirection`.
- Two or three gripper lobes as smooth-union capsules around the core.
- Tool edge as a thin prism/capsule ridge with high-metalness material.
- Small impact sparks as short-lived bead SDFs near the leading edge.
@ -343,11 +568,27 @@ Math:
Movement:
- Running/continuing work leans the core toward the current target.
- Running/continuing work leans the core toward `workAnchor`.
- Changed files or artifacts trigger quick acknowledgement pops.
- Blocked state retracts grippers and dims the tool edge.
- Verification pass or accepted result relaxes the shape back into idle.
Hands spatial rule:
```text
handsForward = workDirection
handsRight = float2(-handsForward.y, handsForward.x)
coreLength = lerp(1.15, 1.65, activity)
coreRadius = lerp(0.46, 0.38, activity)
toolEdgePos = handsCenter.xy + handsForward * coreLength * 0.48
gripPosA = handsCenter.xy - handsForward * 0.18 + handsRight * 0.34
gripPosB = handsCenter.xy - handsForward * 0.18 - handsRight * 0.34
```
`workDirection` is not semantic mood. It is derived from `workAnchor` in the
coordinate contract above. If all work anchors are missing, it falls back to the
clockwise orbit tangent, so the shape remains deterministic and debuggable.
Surfaces:
- `epiphany.surface.roles`: `implementation` status, note, jobs.
@ -376,7 +617,8 @@ Math:
small radius.
- Inner core sphere visible through material contrast, not real transparency
dependency.
- Risk seam as a thin torus/capsule ring around the crystal.
- Risk seam as a thin torus/capsule ring around the crystal. Ring thickness is
`lerp(0.015, 0.09, risk)`.
- Finding marks as tiny engraved line SDFs or bright dots on facets.
- Material ids: cool crystal facets, white inner core, red risk seam, blue
confidence edge, dark rejected mark.
@ -388,6 +630,9 @@ Movement:
- Confidence brightens blue edges and steadies rotation.
- Failed or blocked findings produce short angular fractures; accepted findings
heal them into clean facet lines.
- Review-facing posture points the risk seam normal toward
`roleAnchor(coordinator)` when `requiresReview` is true, otherwise toward
`outwardVector`.
Surfaces:
@ -428,6 +673,9 @@ Movement:
- Regather opens a dark seam and pulls beads inward.
- Resume steadies the ember and releases beads along the old path.
- Sleep/incubation slows orbit and dims the shell while preserving the ember.
- Memory bead trail follows a logarithmic spiral centered on Life's
`bodyCenter.xy`. Regather reverses bead phase toward the seed; resume advances
phase outward along `continuityAnchor -> lifePosition` tangent.
Surfaces:

View file

@ -17,3 +17,4 @@
{"timestamp":"2026-05-11T08:10:00+01:00","topic":"rider-solution-compatibility","lesson":"Rider may open Aquarium.Engine.slnx with empty project trees even though dotnet can build it. Keep Aquarium.Engine.sln as the Rider-facing classic solution while Aquarium.Engine.slnx remains the compact CLI solution.","files":["Aquarium.Engine.sln","Aquarium.Engine.slnx","README.md","state/map.yaml"]}
{"timestamp":"2026-05-11T08:35:00+01:00","topic":"epiphany-agent-sdf-visual-language","lesson":"Epiphany agent bodies should be role-specific SDF organs driven by published CultNet surfaces: identity from epiphany.agent_memory, motion from epiphany.agent_heartbeat, posture from surface roles/coordinator/role_result, Face output from surface.face, and continuity pressure from pressure/reorient surfaces. Body and Imagination are the first implementation pair because they exercise grounded graph mass and generative harmonic bloom against the same AgentVisualState contract.","files":["docs/epiphany-agent-sdf-visual-language.md","state/map.yaml","state/memory.json"],"source":"E:\\Projects\\EpiphanyAgent\\schemas\\README.md"}
{"timestamp":"2026-05-11T08:48:00+01:00","topic":"agent-expression-placement","lesson":"Agent orbit lanes are home anchors, not cages. Role bodies may wander a bounded distance from their designated orbit slots for expression, and vertical animation should come from CPU-visible gravity-well pulses so Grid height, body centers, shadows, and temporal reprojection share the same placement truth.","files":["docs/epiphany-agent-sdf-visual-language.md","state/map.yaml","state/memory.json"]}
{"timestamp":"2026-05-11T09:02:00+01:00","topic":"agent-visual-spec-specificity","lesson":"The Epiphany agent visual-language doc must specify spatial derivation, not describe role bodies with directionless vibes. Agent SDF orientation now has a world coordinate contract, fixed home orbit slots, named anchors, fallback chains, normalized state scalars, and concrete workDirection derivation for Hands.","files":["docs/epiphany-agent-sdf-visual-language.md"],"source":"user review"}