Documentation
¶
Overview ¶
Package geppetto is the stateless, deterministic Utility AI core used by geppetto. It turns an agent's considerations and the actions advertised by nearby providers into a selected action. The package owns scoring, eligibility, stochastic selection, simulation ticks, preemption, and the deterministic reconciliation of contested batch decisions. It does not discover world entities, open transports, persist NPC state, or apply the actual resource debit performed by a game.
Games normally construct a Decider with WithProfile or WithTuning and pass an explicit seed to Decide, Rank, or DecideBatch. Timed simulation is a separate mutable-agent lifecycle: construct a Simulator with the same options and pass an explicit seed to its Advance method. The separation keeps a point-in-time decision from accidentally owning a game's NPC state, while making both deterministic paths equally available in-process.
The architecture is intentionally an inversion of the usual scripted-NPC relationship. An agent does not contain a table such as "if hungry, find a refrigerator". Objects, places, events, and other actors advertise what they offer through AdvertisedAction values. The agent scores those promises against its current considerations. A new world object therefore adds an advertisement rather than a new branch in every NPC's code: the content surface grows approximately with N+M instead of requiring N*M agent/object integrations.
Considerations and their updaters ¶
A Consideration is a bounded scalar signal such as HUNGER, ENERGY, THREAT, AMMO, COVER, or a game-specific mission value. The engine treats all of these uniformly. The updater, rather than the consideration name, explains how a value changes:
- LinearDecay subtracts rate*deltaHours, and LinearRegen adds it.
- EventDriven values are left untouched by a tick; the game changes them through explicit events or action effects.
- PerceptionDriven, RelationshipDriven, and ContextAggregate values read the corresponding World map, or call their configured Value function. If a map has no entry for the consideration, its previous value is retained rather than silently reset to zero.
Every result is clamped to the consideration's own Min and Max. A missing or invalid range (Max <= Min) makes its response pressure zero, so profiles must declare bounds explicitly. Trait consideration deltas are additive rate adjustments and affect only the linear updater kinds. ApplyGradualDeltas delivers an action's promised deltas at a constant rate over its estimated duration, also clamping after each application; a delta for a consideration the agent does not have is ignored.
Response curves turn a raw value into pressure before scoring. Convex curves use a clamped normalized distance from the maximum raised to an exponent; Linear curves use that normalized distance directly; Step curves switch between Below and Above at Threshold; Logistic curves transition around a Midpoint. A convex exponent of zero inherits the profile default (normally 2), while a configured exponent is an intentional per-consideration override. An unknown curve kind evaluates to zero rather than creating an implicit rule.
DefaultTuning supplies the reference calibration: convex exponent 2, intrinsic-priority weight 5, distance reference 10 world units, selection TopK 3, temperature 1, reconciliation TopK 5, preemption margin 1.5, convention-break probability 0, perception noise 0, a FULL tick of one minute of game time, and a SIMPLIFIED tick of one hour. For parameters that use zero as "not configured", zero falls back to these defaults; it is not a way to disable priority or stochastic selection. In particular, a profile cannot request a true zero temperature through this fallback mechanism. Profiles can still choose genre-specific values: social and open-world profiles use higher temperatures and preemption margins for emergent, stubborn behavior; tactical profiles use lower values and rely more on perception noise; survival profiles give stronger weights to hunger and thirst.
The value zero in action capacity is not the same kind of zero as provider capacity. An AdvertisedAction with Capacity == 0 has no action-level limit; a provider with Capacity == 0 has no available provider slots. The action meaning is deliberate: proto3 encodes an omitted numeric field as zero, and older clients omit action_capacities. Treating that zero as blocking would make every pre-existing action unusable after the field was added. The provider meaning remains blocking. An agent must have a free slot at both levels, and the stricter level wins.
Utility scoring ¶
Candidates first pass binary eligibility gates. The provider must have a free slot, the action must not be saturated, the provider must be within the action's inclusive advertisement radius, all preconditions must hold, the cost must be payable, and no active narrative commitment may contradict the action tags. Failed gates remove a candidate; they do not give it a lower score.
For an eligible action x and agent A, the implementation computes:
benefit = sum(pressure(c) * expectedGain(c, x))
score = (benefit + intrinsicPriority(x)*default priority weight)
* M_personality * M_context * M_distance
- weightedCost
The expected gain is saturated per consideration:
expectedGain(c, x) = min(delta(x,c), c.Max-c.Value)
This cap prevents an oversized promise from dominating when an agent has little left to gain. It is intentionally asymmetric: it limits positive gains only. A negative delta remains a real cost to the consideration, because Max-Value is non-negative and cannot clip that negative promise. This is how mixed actions, such as an enjoyable but tiring activity, remain meaningful without a special case.
Each factor has a distinct design job. Pressure makes urgency nonlinear, so a critical need can outweigh a low-value comfort without a hard-coded mode switch. Intrinsic priority lets content authors express a structural reason to prefer an action, but it is added before the other multipliers and thus still obeys distance. Personality multiplies tag modifiers and the domain preference, allowing traits to amplify or suppress a whole promise without making an action attractive when it helps none of the agent's considerations. Distance uses the hyperbolic multiplier 1/(1+d/default distance reference), making a nearby modest opportunity compete with a distant ideal one. Cost is subtracted last so positive personality, context, or distance modifiers cannot make a resource bill disappear.
Context has an important, deliberate asymmetry in this implementation. For every active Context, the tag multiplier is applied twice:
M_context = product(contextTagMultiplier * contextTagMultiplier)
Thus a nominal context multiplier of 2 for an action tag contributes 4 to the score. Applying the multiplier only once lets the effect dissolve in the later softmax and does not produce the intended doubling of observable action frequency. Do not simplify this to a single factor without recalibrating the behavior and its acceptance expectations.
weightedCost is also more specific than the name might suggest. The real implementation is:
sum(cost / max(1, available/cost))
For an eligible positive cost, that is cost^2/available. Doubling the cost therefore quadruples its penalty, while halving the available stock doubles the penalty for the same cost. Eligibility checks affordability first; the score then models scarcity. The penalty is not multiplied by the positive factors above.
Selection and deliberate imperfection ¶
Candidates are stable-sorted by descending utility. SelectAction keeps the configured SelectionTopK (default 3) and samples with a numerically stable softmax. The maximum utility is subtracted before exponentiation, which preserves the distribution while avoiding overflow. A non-positive SelectionTemperature falls back to the default 1.0; temperature zero is therefore not an accidental way to request argmax. Lower temperatures make choices more competent and predictable, while higher temperatures flatten the differences. A TopK of one degenerates to argmax.
Imperfection is a design principle, not an implementation defect. Always taking the maximum makes NPCs predictable, exploitable, and narratively sterile. The stochastic pick is retained as the first preference in a batch so that contention resolution does not silently turn the system back into argmax. Commitment to a current action, limited advertisement radius, and personality or context biases provide further, tunable sources of behavior that is imperfect but still legible. PerceptionNoise adds zero-mean Gaussian noise with the configured standard deviation to each perception-driven consideration in a temporary scoring copy; the observed world state remains unchanged. ConventionBreakProbability defines the normal convention as the highest-utility eligible action. When it succeeds, the normal softmax pick is replaced by a uniformly selected eligible action with strictly lower utility, when one exists. The aim is variety that an observer can explain, not minimum error.
Ticks, preemption, and simulation detail ¶
Advance is the in-process entry point. Its TickRequest carries the absolute simulation hour, world, desired simulation level, and optional simplified event; TickResult reports the effective level and simulated duration. The integrating simulation chooses the target level because only it knows visibility and importance. FullTickHours and SimplifiedTickHours determine the duration returned for their respective levels, falling back to the named engine defaults when unset.
Advance first expires commitments whose ExpiresAt is at or before NowHours. A simplified advance records its optional event at NowHours. Returning to Full applies the profile's aggregated-event effects before the detailed interval. Tick then updates considerations, applies gradual deltas to the current action, and clears a completed action. An idle agent dequeues the next FIFO ActionQueue entry, or makes an autonomous selection when no work is queued; a continuing action is then considered for preemption. A newly started action therefore does not receive its first gradual delta until the following interval. The engine owns queue progression through EnqueueAction, while the integrating game remains responsible for enqueuing intent and debiting costs. Preemption abandons the current action rather than requeueing partial work; pending FIFO entries remain available after the replacement action finishes.
Preemption requires at least one consideration below its configured CriticalThreshold. A player-queued action is protected unless some value reaches imminent collapse, read from that consideration's own CriticalThreshold; a consideration without one falls back to the engine's default imminent-collapse threshold. The replacement is selected through the same stochastic pipeline, and it must strictly exceed preemptionMargin times the current action's frozen ContinuationUtility. Equality does not preempt. This frozen utility is the snapshot captured when the action was selected, so an action that was very valuable when started can be deliberately difficult to interrupt.
ApplyAggregatedEvent records coarse events while an agent is simplified; TransitionToFull applies the profile's AggregatedEventEffects table to the events that occurred by the target hour, clamping each resulting consideration value, then discards the event log and returns the agent to Full. The table is data: the engine knows neither event nor consideration names, and a profile without it reconstructs nothing. The mechanism is intentionally small and stateless: the game remains responsible for deciding which distant events to aggregate.
Batch contention and determinism ¶
RankedPreferences performs the normal stochastic selection first, then retains descending-utility fallbacks up to ReconciliationTopK (default 5). ResolveContention consumes those lists in rounds. Every open agent claims its current candidate; claims are grouped by provider, and each provider's contenders are ordered by increasing physical distance, then agent ID, then input index. Provider slots are Capacity minus the reported Occupants count. Action slots are tracked per provider instance and start at action Capacity minus action Occupancy; zero action capacity means unlimited. A candidate is granted only if both levels have a slot.
Proximity, not utility, decides a dispute. This is a physical rule: the agent that can reach the resource first gets it. Hunger does not accelerate anyone, so a hungry distant agent must not teleport priority away from a nearby agent merely because its score is higher. A loser advances through its own preference list. That can displace a provisional winner elsewhere, so rounds continue until one complete round has no displacement. An agent that exhausts its list receives no ActionInstance. Because every cursor only moves forward through a finite retained list, termination is guaranteed.
Stability ¶
Within a single version of this module, the same input and the same seed always produce the same result. That guarantee is tested, and it is the whole guarantee. Across versions there is no determinism commitment: the scoring formula and its calibration may change in any minor release, and the same seed may then select a different action. A game that needs deterministic replay or save compatibility must pin the module version.
The scoring phase is independent and can run in parallel; the reconciliation phase is a small serial reduction over bounded lists. Map iteration order and worker completion order cannot change the result: each agent appears in one claim list per round, and the within-provider order is explicit. With the same input and random seed, the selected preferences and final assignments are deterministic. No contention state survives the call.
Index ¶
- func ApplyAggregatedEvent(agent *Agent, event AggregatedEvent)
- func ApplyGradualDeltas(agent *Agent, instance *ActionInstance, deltaHours float64)
- func CheckPreemption(agent *Agent, providers []AffordanceProvider, tuning Tuning, rng *rand.Rand) bool
- func EnqueueAction(agent *Agent, action ActionInstance)
- func ScoreAction(agent Agent, provider AffordanceProvider, action AdvertisedAction, ...) float64
- func Tick(agent *Agent, providers []AffordanceProvider, world World, deltaHours float64, ...)
- func TransitionToFull(agent *Agent, hour float64, effects []AggregatedEventEffect)
- func UpdateConsiderations(agent *Agent, world World, deltaHours float64)
- type ActionInstance
- type AdvertisedAction
- type AffordanceProvider
- type Agent
- type AggregatedEvent
- type AggregatedEventEffect
- type Candidate
- type Consideration
- type ConsiderationUpdater
- type Context
- type Decider
- func (decider Decider) Decide(agent Agent, providers []AffordanceProvider, seed uint64) *ActionInstance
- func (decider Decider) DecideBatch(agents []Agent, providers []AffordanceProvider, seed uint64) []*ActionInstance
- func (decider Decider) Rank(agent Agent, providers []AffordanceProvider, seed uint64) []Candidate
- func (decider Decider) Reconcile(agents []Agent, preferences [][]Candidate) []*ActionInstance
- type NarrativeCommitment
- type Option
- type Personality
- type Position
- type Precondition
- type Profile
- type ResponseCurve
- type ResponseCurveKind
- type SimulationLevel
- type Simulator
- type TickRequest
- type TickResult
- type Trait
- type Tuning
- type UpdaterKind
- type World
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApplyAggregatedEvent ¶
func ApplyAggregatedEvent(agent *Agent, event AggregatedEvent)
ApplyAggregatedEvent records an event produced while an agent uses simplified LOD.
func ApplyGradualDeltas ¶
func ApplyGradualDeltas(agent *Agent, instance *ActionInstance, deltaHours float64)
ApplyGradualDeltas applies an action's promised deltas at a constant rate.
func CheckPreemption ¶
func CheckPreemption(agent *Agent, providers []AffordanceProvider, tuning Tuning, rng *rand.Rand) bool
CheckPreemption replaces the current action when a critical consideration makes the best available action valuable enough to clear the configured margin.
func EnqueueAction ¶
func EnqueueAction(agent *Agent, action ActionInstance)
EnqueueAction appends an integrating game's explicit action request. Queued actions are FIFO and protected from ordinary preemption after they start.
func ScoreAction ¶
func ScoreAction(agent Agent, provider AffordanceProvider, action AdvertisedAction, tuning Tuning) float64
ScoreAction scores an advertised promise. Personality, context, and distance scale benefits; costs remain penalties so a cheap action cannot become expensive through a positive modifier.
func Tick ¶
func Tick(agent *Agent, providers []AffordanceProvider, world World, deltaHours float64, tuning Tuning, rng *rand.Rand)
Tick advances one detailed interval after Advance has chosen Full.
func TransitionToFull ¶
func TransitionToFull(agent *Agent, hour float64, effects []AggregatedEventEffect)
TransitionToFull reconstructs a plausible detailed state from accumulated events. The effects table, declared by the profile, maps each aggregated event kind to the consideration deltas it applies — the engine knows neither event nor consideration names. Events after hour and undeclared kinds are ignored; a nil table reconstructs nothing.
func UpdateConsiderations ¶
UpdateConsiderations advances every updater, applies trait rate adjustments, and clamps each result to its configured bounds.
Types ¶
type ActionInstance ¶
type ActionInstance struct {
Action AdvertisedAction
ProviderID string
Elapsed float64
ContinuationUtility float64
PlayerQueued bool
}
ActionInstance records a selected action and its progress for preemption.
func ResolveContention ¶
func ResolveContention(agents []Agent, preferences [][]Candidate) []*ActionInstance
ResolveContention distributes contested slots to the nearest agents. preferences[i] is agent i's ordered candidate list, as produced by RankedPreferences. It returns one ActionInstance per agent, nil when the agent exhausted its candidates.
Slots are enforced at TWO levels: the provider's (Capacity minus the occupants reported by the client) and the action's own (Capacity minus Occupancy; action capacity 0 means unlimited — see the warning on AdvertisedAction.Capacity). An agent wins its current candidate only with a free slot at both levels; the stricter level wins.
The loop terminates because a displaced agent's cursor only moves forward through its own finite list: each agent is displaced from a given candidate at most once, so total displacements are bounded by the sum of the list lengths.
func SelectAction ¶
func SelectAction(agent Agent, providers []AffordanceProvider, tuning Tuning, rng *rand.Rand) *ActionInstance
SelectAction selects one of the highest-scoring candidates with softmax.
type AdvertisedAction ¶
type AdvertisedAction struct {
ActionID string `json:"action_id"`
Deltas map[string]float64 `json:"deltas"`
EstimatedDuration float64 `json:"estimated_duration"`
Tags []string `json:"tags"`
Domain string `json:"domain"`
Preconditions []Precondition `json:"preconditions"`
IntrinsicPriority float64 `json:"intrinsic_priority"`
AdvertisementRadius float64 `json:"advertisement_radius"`
Cost map[string]float64 `json:"cost"`
// Capacity caps simultaneous agents on THIS action. WARNING: unlike
// AffordanceProvider.Capacity, zero means UNLIMITED — proto3 encodes an
// absent numeric field as 0 and every pre-existing client sends 0, so 0
// must read as "no own limit" or every existing action would brick. An
// agent joins only with a free slot at both levels; the stricter wins.
Capacity int `json:"capacity"`
// Occupancy is how many agents the client reports as already executing
// this action at tick start. It consumes action slots before the batch
// is reconciled.
Occupancy int `json:"occupancy"`
}
AdvertisedAction is a provider's declarative promise of consideration deltas.
type AffordanceProvider ¶
type AffordanceProvider struct {
ID string `json:"id"`
Position Position `json:"position"`
AdvertisedActions []AdvertisedAction `json:"advertised_actions"`
Capacity int `json:"capacity"`
Occupants []string `json:"occupants"`
State string `json:"state"`
}
AffordanceProvider advertises actions at a world position with limited capacity.
type Agent ¶
type Agent struct {
ID string
Considerations map[string]Consideration
Personality Personality
CurrentAction *ActionInstance
// ActionQueue holds FIFO actions supplied through EnqueueAction. The engine
// starts its head whenever the agent becomes idle.
ActionQueue []ActionInstance
Position Position
Capabilities map[string]bool
ActiveContexts []Context
SimulationLevel SimulationLevel
NarrativeCommitments []NarrativeCommitment
Resources map[string]float64
AggregatedEvents []AggregatedEvent
}
Agent contains all state the engine needs to update and score one NPC.
type AggregatedEvent ¶
AggregatedEvent records a coarse simulation event for a simplified agent.
type AggregatedEventEffect ¶
type AggregatedEventEffect struct {
// Kind matches AggregatedEvent.Kind.
Kind string `json:"kind"`
// Deltas are added to the matching considerations, clamped to their bounds.
Deltas map[string]float64 `json:"deltas"`
}
AggregatedEventEffect declares how one kind of aggregated event rebuilds consideration state when an agent returns to full simulation detail. The profile owns the table; the engine applies it without knowing event or consideration names.
type Candidate ¶
type Candidate struct {
Action AdvertisedAction
Provider AffordanceProvider
Utility float64
}
Candidate couples an eligible advertised action with its scored utility.
func Candidates ¶
func Candidates(agent Agent, providers []AffordanceProvider, tuning Tuning) []Candidate
Candidates returns all eligible actions in descending utility order.
func RankedPreferences ¶
func RankedPreferences(agent Agent, providers []AffordanceProvider, tuning Tuning, rng *rand.Rand) []Candidate
RankedPreferences returns the agent's candidates ordered for contention resolution: the stochastic softmax pick first (identical to what SelectAction would choose with the same rng), then the remaining candidates in descending utility as fallbacks, capped at the configured limit.
type Consideration ¶
type Consideration struct {
ID string `json:"id"`
Value float64 `json:"value"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Updater ConsiderationUpdater `json:"updater"`
BaseWeight float64 `json:"base_weight"`
CriticalThreshold float64 `json:"critical_threshold"`
ResponseCurve ResponseCurve `json:"response_curve"`
}
Consideration is a normalized NPC signal, its bounds, and its scoring rule.
type ConsiderationUpdater ¶
type ConsiderationUpdater struct {
Kind UpdaterKind `json:"kind"`
Rate float64 `json:"rate,omitempty"`
// Value is an optional in-process extension point for the world-driven
// kinds (PerceptionDriven, RelationshipDriven, ContextAggregate). When
// set, a tick calls it instead of reading the corresponding World map,
// so a game can derive the consideration from state the JSON profile
// format cannot express — line of sight, faction standing, inventory
// contents — by closing over its own world representation. The function
// must be a pure, deterministic function of the supplied agent and
// world: it runs inside the simulation tick, and determinism guarantees
// assume it. Value is ignored by the linear and event-driven kinds and
// cannot be loaded from a profile document.
Value func(Agent, World) float64 `json:"-"`
}
ConsiderationUpdater defines the dynamics applied to a Consideration.
type Context ¶
type Context struct {
Modifiers map[string]float64 `json:"modifiers"`
Duration float64 `json:"duration"`
}
Context temporarily biases actions carrying matching tags.
type Decider ¶
type Decider struct {
// contains filtered or unexported fields
}
Decider selects actions from a supplied agent and provider snapshot. It has no mutable simulation state; pass an explicit seed to every operation so independent decisions remain reproducible.
func NewDecider ¶
NewDecider constructs a decision API. WithProfile supplies its tuning, and WithTuning is useful when a caller owns model data without a JSON profile.
func (Decider) Decide ¶
func (decider Decider) Decide(agent Agent, providers []AffordanceProvider, seed uint64) *ActionInstance
Decide selects an action for one agent from the supplied provider snapshot. The seed owns the complete pseudo-random stream for this decision.
Example ¶
A hungry villager scores the advertisements around it and the kitchen wins. The same seed always reproduces the same choice.
package main
import (
"fmt"
"github.com/Otoru/geppetto"
)
// villageProfile is the smallest profile that still shows pressure, decay,
// and competing advertisements.
func villageProfile() geppetto.Profile {
consideration := func(id string, value float64) geppetto.Consideration {
return geppetto.Consideration{
ID: id, Value: value, Min: -100, Max: 100, BaseWeight: 1,
CriticalThreshold: -50,
Updater: geppetto.ConsiderationUpdater{Kind: geppetto.LinearDecay, Rate: 5},
ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2},
}
}
return geppetto.Profile{
Name: "village",
Considerations: []geppetto.Consideration{
consideration("HUNGER", 0),
consideration("ENERGY", 0),
},
Tuning: geppetto.DefaultTuning(),
}
}
func main() {
profile := villageProfile()
decider := geppetto.NewDecider(geppetto.WithProfile(profile))
villager := geppetto.Agent{
ID: "bram",
Considerations: map[string]geppetto.Consideration{
"HUNGER": {ID: "HUNGER", Value: -80, Min: -100, Max: 100, BaseWeight: 1, ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2}},
"ENERGY": {ID: "ENERGY", Value: 50, Min: -100, Max: 100, BaseWeight: 1, ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2}},
},
}
providers := []geppetto.AffordanceProvider{
{
ID: "kitchen", Capacity: 2,
AdvertisedActions: []geppetto.AdvertisedAction{{
ActionID: "eat", Deltas: map[string]float64{"HUNGER": 40},
EstimatedDuration: 1, AdvertisementRadius: 50,
}},
},
{
ID: "bench", Capacity: 1,
AdvertisedActions: []geppetto.AdvertisedAction{{
ActionID: "rest", Deltas: map[string]float64{"ENERGY": 30},
EstimatedDuration: 1, AdvertisementRadius: 50,
}},
},
}
chosen := decider.Decide(villager, providers, 42)
fmt.Printf("bram chooses %s at %s\n", chosen.Action.ActionID, chosen.ProviderID)
}
Output: bram chooses eat at kitchen
func (Decider) DecideBatch ¶
func (decider Decider) DecideBatch(agents []Agent, providers []AffordanceProvider, seed uint64) []*ActionInstance
DecideBatch ranks each agent with seed plus its stable slice index, then reconciles contested capacity. It is a convenience for callers that do not need their own parallel ranking phase.
type NarrativeCommitment ¶
type NarrativeCommitment struct {
ID string `json:"id"`
ContradictoryTags map[string]bool `json:"contradictory_tags"`
ExpiresAt float64 `json:"expires_at"`
}
NarrativeCommitment prevents autonomous actions with contradictory tags. ExpiresAt is an absolute simulation hour; zero means the commitment does not expire.
type Option ¶
type Option func(*options)
Option configures a Decider or Simulator. Options are immutable values and may be reused when constructing multiple SDK objects.
func WithProfile ¶
WithProfile supplies the profile used for tuning and aggregated-event reconstruction. WithTuning overrides only the profile's tuning values.
func WithTuning ¶
WithTuning supplies tuning without requiring a profile. When combined with WithProfile, this option overrides Profile.Tuning regardless of option order.
type Personality ¶
type Personality struct {
Preferences map[string]float64 `json:"preferences"`
Traits []Trait `json:"traits"`
}
Personality collects an agent's domain preferences and traits.
type Position ¶
type Position struct{ X, Y, Z float64 }
Position is a three-dimensional point in world units.
type Precondition ¶
type Precondition struct {
Capability string `json:"capability,omitempty"`
Resource string `json:"resource,omitempty"`
Minimum float64 `json:"minimum,omitempty"`
// RequiredState must match the AffordanceProvider state when it is non-empty.
RequiredState string `json:"required_state,omitempty"`
}
Precondition is an agent capability/resource requirement or a provider-state requirement for an AdvertisedAction.
type Profile ¶
type Profile struct {
Name string `json:"name"`
Considerations []Consideration `json:"considerations"`
Tuning Tuning `json:"tuning"`
AggregatedEventEffects []AggregatedEventEffect `json:"aggregated_event_effects,omitempty"`
}
Profile defines the consideration order and tuning shared by a decision batch or simulation. AggregatedEventEffects rebuild detailed state after a simplified simulation interval.
type ResponseCurve ¶
type ResponseCurve struct {
Kind ResponseCurveKind `json:"kind"`
// Exponent overrides the profile default for this convex consideration.
Exponent float64 `json:"exponent,omitempty"`
Threshold float64 `json:"threshold,omitempty"`
Below float64 `json:"below,omitempty"`
Above float64 `json:"above,omitempty"`
Slope float64 `json:"slope,omitempty"`
Midpoint float64 `json:"midpoint,omitempty"`
}
ResponseCurve configures the response-curve parameters for a Consideration.
func (ResponseCurve) Evaluate ¶
func (r ResponseCurve) Evaluate(value, minimum, maximum float64) float64
Evaluate converts value into the normalized pressure defined by r. A convex curve without an explicit exponent resolves through the same default path as the scoring pipeline, with an empty tuning.
type ResponseCurveKind ¶
type ResponseCurveKind string
ResponseCurveKind identifies the function used to turn a consideration value into a normalized pressure.
const ( // Convex increases pressure rapidly as a consideration approaches its minimum. Convex ResponseCurveKind = "convex" // Linear changes pressure proportionally to the consideration value. Linear ResponseCurveKind = "linear" // Step switches pressure at the curve threshold. Step ResponseCurveKind = "step" // Logistic transitions pressure smoothly around the curve midpoint. Logistic ResponseCurveKind = "logistic" )
type SimulationLevel ¶
type SimulationLevel string
SimulationLevel identifies whether the agent receives detailed or aggregated updates.
const ( // Full runs the detailed per-tick decision loop. Full SimulationLevel = "FULL" // Simplified represents an agent with aggregated, low-detail events. Simplified SimulationLevel = "SIMPLIFIED" )
type Simulator ¶
type Simulator struct {
// contains filtered or unexported fields
}
Simulator advances mutable agent state through the timed simulation loop. It retains the profile's aggregation table but no agent state or random state, so callers can safely share it across independent agents.
func NewSimulator ¶
NewSimulator constructs a timed simulation API. WithProfile provides both event reconstruction data and tuning; WithTuning overrides its tuning.
func (Simulator) Advance ¶
func (simulator Simulator) Advance(agent *Agent, providers []AffordanceProvider, request TickRequest, seed uint64) TickResult
Advance advances agent through one complete timed simulation step. The seed owns the random stream for this step, including any selection or preemption.
Example ¶
One hour per tick: hunger and energy decay, the agent eats, then rests, then eats again as pressures shift. Every tick passes its own seed.
package main
import (
"fmt"
"github.com/Otoru/geppetto"
)
// villageProfile is the smallest profile that still shows pressure, decay,
// and competing advertisements.
func villageProfile() geppetto.Profile {
consideration := func(id string, value float64) geppetto.Consideration {
return geppetto.Consideration{
ID: id, Value: value, Min: -100, Max: 100, BaseWeight: 1,
CriticalThreshold: -50,
Updater: geppetto.ConsiderationUpdater{Kind: geppetto.LinearDecay, Rate: 5},
ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2},
}
}
return geppetto.Profile{
Name: "village",
Considerations: []geppetto.Consideration{
consideration("HUNGER", 0),
consideration("ENERGY", 0),
},
Tuning: geppetto.DefaultTuning(),
}
}
func main() {
profile := villageProfile()
profile.Tuning.FullTickHours = 1
simulator := geppetto.NewSimulator(geppetto.WithProfile(profile))
villager := geppetto.Agent{
ID: "marta",
Considerations: map[string]geppetto.Consideration{
"HUNGER": {ID: "HUNGER", Value: -70, Min: -100, Max: 100, BaseWeight: 1, CriticalThreshold: -50, Updater: geppetto.ConsiderationUpdater{Kind: geppetto.LinearDecay, Rate: 5}, ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2}},
"ENERGY": {ID: "ENERGY", Value: -40, Min: -100, Max: 100, BaseWeight: 1, CriticalThreshold: -50, Updater: geppetto.ConsiderationUpdater{Kind: geppetto.LinearDecay, Rate: 5}, ResponseCurve: geppetto.ResponseCurve{Kind: geppetto.Convex, Exponent: 2}},
},
}
providers := []geppetto.AffordanceProvider{
{
ID: "kitchen", Capacity: 1,
AdvertisedActions: []geppetto.AdvertisedAction{{
ActionID: "eat", Deltas: map[string]float64{"HUNGER": 70},
EstimatedDuration: 1, AdvertisementRadius: 50,
}},
},
{
ID: "bench", Capacity: 1,
AdvertisedActions: []geppetto.AdvertisedAction{{
ActionID: "rest", Deltas: map[string]float64{"ENERGY": 60},
EstimatedDuration: 1, AdvertisementRadius: 50,
}},
},
}
for tick := 1; tick <= 3; tick++ {
simulator.Advance(&villager, providers, geppetto.TickRequest{NowHours: float64(tick)}, 42+uint64(tick))
fmt.Printf("tick %d: HUNGER=%.0f ENERGY=%.0f action=%s\n",
tick,
villager.Considerations["HUNGER"].Value,
villager.Considerations["ENERGY"].Value,
villager.CurrentAction.Action.ActionID)
}
}
Output: tick 1: HUNGER=-75 ENERGY=-45 action=eat tick 2: HUNGER=-10 ENERGY=-50 action=rest tick 3: HUNGER=-15 ENERGY=5 action=eat
type TickRequest ¶
type TickRequest struct {
World World
NowHours float64
TargetLevel SimulationLevel
AggregatedEvent *AggregatedEvent
}
TickRequest describes one in-process Advance call. NowHours is the absolute simulation hour at which expiry and aggregate-event timestamps are evaluated. TargetLevel is selected by the integrating simulation; an empty value keeps the agent's current level (or Full for a new agent). AggregatedEvent is recorded only while the target level is Simplified.
type TickResult ¶
type TickResult struct {
SimulationLevel SimulationLevel
ElapsedHours float64
}
TickResult reports the effective level and simulated duration of one Advance call. The caller schedules its next advance after ElapsedHours.
func Advance ¶
func Advance(agent *Agent, profile Profile, providers []AffordanceProvider, request TickRequest, rng *rand.Rand) TickResult
Advance runs one complete simulation step. It first expires commitments so expired restrictions cannot block this step. A simplified step records its optional coarse event and returns its aggregate cadence. A transition back to Full reconstructs accumulated events before the detailed tick updates considerations, applies the current action, then either starts queued work or selects a new action for an idle agent, or evaluates preemption for a continuing action. This order lets the decision see the state produced by the elapsed interval while a newly started action begins contributing on the following interval.
type Trait ¶
type Trait struct {
ID string `json:"id"`
Modifiers map[string]float64 `json:"modifiers"`
ConsiderationDelta map[string]float64 `json:"consideration_delta"`
}
Trait changes action-tag multipliers and consideration updater rates.
type Tuning ¶
type Tuning struct {
// ResponseCurveExponent is the profile default for convex curves without an override.
ResponseCurveExponent float64 `json:"response_curve_exponent"`
WPriority float64 `json:"W_PRIORITY"`
DistanceReference float64 `json:"DISTANCE_REFERENCE"`
SelectionTopK int `json:"SELECTION_TOP_K"`
SelectionTemperature float64 `json:"SELECTION_TEMPERATURE"`
// ReconciliationTopK caps how many ordered candidates each agent retains
// for contention resolution: the stochastic first pick plus
// utility-ordered fallbacks.
ReconciliationTopK int `json:"RECONCILIATION_TOP_K"`
PreemptionMargin float64 `json:"preemption_margin"`
// ConventionBreakProbability replaces the normal choice with a strictly
// lower-utility eligible action when its probability succeeds.
ConventionBreakProbability float64 `json:"convention_break_probability"`
// PerceptionNoise is the standard deviation of Gaussian noise applied to
// perception-driven considerations while scoring a decision.
PerceptionNoise float64 `json:"perception_noise"`
// FullTickHours is the simulated duration of one detailed Advance call.
FullTickHours float64 `json:"full_tick_hours"`
// SimplifiedTickHours is the simulated duration of one aggregated Advance call.
SimplifiedTickHours float64 `json:"simplified_tick_hours"`
}
Tuning collects the profile-level parameters that control engine behavior.
func DefaultTuning ¶
func DefaultTuning() Tuning
DefaultTuning returns the specification's default tuning values.
func (Tuning) WithDefaults ¶ added in v0.2.0
WithDefaults resolves omitted tuning values to the reference calibration. Profile loading calls it once; SDK construction also applies it for callers that construct a Profile or Tuning directly in Go.
type UpdaterKind ¶
type UpdaterKind string
UpdaterKind identifies how a Consideration changes between decisions.
const ( // LinearDecay subtracts Rate on every update. LinearDecay UpdaterKind = "linear_decay" // LinearRegen adds Rate on every update. LinearRegen UpdaterKind = "linear_regen" // EventDriven changes only when an explicit event mutates it. EventDriven UpdaterKind = "event_driven" // PerceptionDriven reads a value from the perceived world. PerceptionDriven UpdaterKind = "perception_driven" // RelationshipDriven derives its value from a relationship. RelationshipDriven UpdaterKind = "relationship_driven" // ContextAggregate derives its value from environmental context. ContextAggregate UpdaterKind = "context_aggregate" )
type World ¶
type World struct {
Perception map[string]float64
Relationships map[string]float64
ContextValues map[string]float64
}
World supplies external values used by consideration updaters. Perception, Relationships, and ContextValues feed their corresponding updater kinds. It is input to a tick, not state owned by the engine.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
geppetto
command
Package main is the geppetto subprocess entry point.
|
Package main is the geppetto subprocess entry point. |
|
internal
|
|
|
config
Package config defines the runtime settings used to wire a geppetto process.
|
Package config defines the runtime settings used to wire a geppetto process. |
|
logging
Package logging builds the process-wide zap logger for geppetto.
|
Package logging builds the process-wide zap logger for geppetto. |
|
service
Package service adapts the protobuf wire contract to the public geppetto SDK.
|
Package service adapts the protobuf wire contract to the public geppetto SDK. |
|
transport
Package transport creates the process listener selected by the command-line configuration.
|
Package transport creates the process listener selected by the command-line configuration. |
|
Package profile loads and validates geppetto JSON profiles.
|
Package profile loads and validates geppetto JSON profiles. |