npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dpid/associative-ai

v0.1.0

Published

LLM-free associative learning (Rescorla-Wagner + eligibility traces) for needs-based AI agents

Readme

@dpid/associative-ai

LLM-free associative learning (Rescorla-Wagner + eligibility traces) for @dpid/needs-based-ai agents. It turns perceived stimuli and need-satisfaction edges into an emergent acquisition/extinction curve — the classic Pavlov's-dog experiment: an agent that hears a bell before every meal starts salivating at the bell alone, and stops if the bell keeps ringing with no food behind it. Needs stay ground truth: the associator only ever produces a raw prediction strength V; a sated dog doesn't salivate at the bell because the consumer gates the response on hunger, not because the association weakened.

Install

npm install @dpid/associative-ai @dpid/needs-based-ai

There are no runtime dependencies. @dpid/needs-based-ai ^0.3.0 is the sole peer dependency (not optional — the event emitter and type surface come from it). @dpid/command-state-machine is a dev-only dependency used to drive the example's tick loop; it is not required to use the library. This is the LLM-free member of the -ai family: no @anthropic-ai/sdk, no web-llm, nothing that talks to a model.

The learning rule

Every reinforcement trial applies the Rescorla-Wagner update to each currently eligible conditioned stimulus (CS):

dV = alpha * beta * (lambda - sumV)

alpha is the CS's salience/learning rate, beta is the unconditioned stimulus (US)'s associability, and lambda is the asymptote (US intensity). The critical detail is sumV: it is the sum of V across every CS present in the trial, read once before any weight is written, and every present CS is updated against that same shared error term. This shared read is what makes blocking and overshadowing emergent — a CS that arrives after another cue has already saturated sumV toward lambda gets almost no error term left to learn from, with no special-cased "blocking" logic anywhere in the code.

The second load-bearing rule is contiguity: a CS's eligibility trace must age at least one tick (ageSeconds > 0) before it counts as "present" for a reinforcement trial. This makes learning forward-only with no explicit direction guard — perceiving a stimulus in the same tick as the outcome, or after it, does not learn; only a stimulus perceived strictly before the outcome (and still within traceWindowSeconds) does.

Usage

import { Associator } from '@dpid/associative-ai';

const assoc = Associator.create({ alpha: 0.3, beta: 0.5, lambda: 1 });

assoc.perceive('bell');   // refresh the bell's eligibility trace
assoc.update(dt);         // age traces by dt; the bell trace is now eligible
assoc.reinforce('food');  // one learning trial over every live, eligible CS

assoc.strengthOf('bell', 'food'); // read V(bell -> food)

Wiring the two integration seams

  • Perceive from the needs-based-ai perception seam, not from game logic: subscribe to agent.onAdvertisementReceived and call associator.perceive(trait.id) for each trait on the advertisement. This is the same event surface a vanilla needs-based agent already receives, so no extra sensing code is needed.
  • Reinforce on the need-satisfaction edge, never per-tick. A multi-tick "eat" action should call reinforce('food') once, at the moment satisfaction happens — not once per frame while the action is in progress. reinforce() is intentionally trial-triggered, not time-triggered.
  • Express the conditioned response by listening to onConditionedResponse and gating it on the agent's live motivational state: magnitude = prediction * g(hunger), where g is whatever hunger-to-gain mapping the game defines. The associator never reads hunger itself — that gate belongs entirely to the consumer, which is why a sated agent can have a high strengthOf('bell', 'food') and still not react.
  • Approach behavior (the agent choosing to move toward the bell) is not part of this module either. Drive it through needs-based-ai's own ranking hooks: broadcast a phantom Advertisement for the CS, or scale a customRankingFunction's weight by strengthOf(csId, usId), so a strongly conditioned cue competes for attention the same way a real need-satisfying object would.

Config

Associator.create(config?: AssociatorConfig) — every field is optional.

| Field | Meaning | Default | | --- | --- | --- | | alpha | CS salience / learning rate. A single number applies to every CS, or pass a Record<string, number> to give specific stimuli their own salience. | 0.3 | | beta | US associability — how strongly a given unconditioned stimulus drives learning. | 0.5 | | lambda | Asymptote that V approaches; conventionally the US's intensity. | 1 | | traceWindowSeconds | How long a perceived stimulus stays eligible for reinforcement after perceive(). | 2 | | traceDecayPerSecond | Exponential decay rate of a trace's eligibility while it's within the window. | 1 | | responseThreshold | The V value at/above which onConditionedResponse fires. | 0.5 | | extinguishOnUnpairedCs | If true, a CS trace that expires without ever being reinforced runs one lambda = 0 trial — this is what drives extinction when a CS is presented alone. | true |

Example

npm run example:dog-bell

Runs the headless Pavlov's-dog demo offline (examples/dog-bell/main.ts, executed via tsx, no network): 30 forward bell-then-food pairings followed by 15 bell-alone rounds, printing V(bell -> food) as both a number and an ASCII bar each trial. The tick loop is driven through @dpid/command-state-machine — a CommandableState runs a command whose onUpdate(dt) calls assoc.update(dt), and the example advances the world with stateMachine.update(dt) rather than calling update() directly, mirroring how a game would drive it. Trimmed sample output:

ACQUISITION -- forward pairings (perceive bell, age one tick, then reinforce food)
  acq  1/30  V=0.150  [######..............|...................]
  acq  5/30  V=0.556  [######################..................]

  🔔→🤤 salivation (prediction=0.56)
     the bell alone now predicts food -- the reflex has emerged mid-acquisition

  acq 30/30  V=0.992  [########################################]

EXTINCTION -- bell presented alone, no reinforcement
  ext  1/15  V=0.844  [##################################......]
  ext 15/15  V=0.087  [###.................|...................]

SUMMARY
  peak V during acquisition:  0.992
  final V after extinction:   0.087 (below threshold 0.5)

Scripts

npm test                  # Run tests with vitest
npm run typecheck         # Type check the library and the example with tsc
npm run build             # Build with tsup (ESM + CJS + d.ts)
npm run example:dog-bell  # Run the offline dog-bell demo via tsx

Design note

This module is drive-agnostic on purpose. It has no concept of hunger, sleep, or any other need — it only tracks CS -> US prediction strength. A conditioned response is association x motivational state; the associator supplies the association, and the game (or @dpid/needs-based-ai, via the seams above) owns the motivational gate.