Psi-theory is a cognitive architecture that models how drives and emotions guide autonomous behaviour.1 Gleam is a strongly typed functional language that compiles to Erlang bytecode and runs on the BEAM virtual machine.2 By blending them we can prototype software agents that tend their own codebases in the resilient, self-healing manner championed across Ümblicøn.
This tutorial uses concepts from the MicroPsi reference implementation by Joscha Bach3 to build a small but complete Gleam project. You’ll set up motivational variables, schedule processes on the BEAM and hook the resulting agent into a development workflow.
Prerequisites
- Gleam and Erlang/OTP installed
- Familiarity with command line tools such as
git
Create a new project:
gleam new psi_agent && cd psi_agent
gleam add gleam_erlang
The generated src
directory is where we’ll add the agent.
1. Represent the agent’s motivational needs
In MicroPsi each need has a current level, an ideal level and an urgency. When the difference between current and ideal grows, the need requests attention. We can model this with a Gleam record and helper functions:
pub type Need {
Need(
name: String,
level: Float,
target: Float,
urgency: Float,
)
}
pub fn update(Need(..) = need, delta: Float) -> Need {
// Move current level toward target by delta
need<-level(need.level + delta)
}
pub fn deviation(Need(..) = need) -> Float {
need.target - need.level
}
deviation
gives the motive power of a need. In a fuller MicroPsi system these values also influence global modulators like arousal and confidence; we’ll track a minimal set:
pub type Modulators {
Modulators(arousal: Float, pleasure: Float)
}
pub type Agent {
Agent(needs: List(Need), modulators: Modulators)
}
pub fn new() -> Agent {
Agent(needs: [], modulators: Modulators(0.5, 0.0))
}
2. Perception: sensors update needs
MicroPsi agents perceive through sensors that directly manipulate needs. In a development context sensors might read file system changes or build results. For demonstration we’ll simulate a timer that slowly decreases a maintenance drive:
import gleam/erlang/process
import gleam/erlang/time
pub fn start(agent: Agent) {
process.spawn(fn() { monitor(agent) })
}
fn monitor(agent: Agent) {
let updated_agent =
agent
|> update_need("maintenance", fn(n) { update(n, -0.01) })
time.sleep(1000)
monitor(updated_agent)
}
pub fn update_need(name: String, f: fn(Need) -> Need, Agent(..) = agent) -> Agent {
let needs =
agent.needs
|> list.map(fn(n) {
case n.name == name {
True -> f(n)
False -> n
}
})
agent<-needs(needs)
}
The monitor
loop runs as an independent BEAM process. Each tick lowers the maintenance drive, eventually triggering action selection.
3. Decision cycle and action selection
MicroPsi evaluates needs and picks intentions through an ongoing cognitive cycle. We’ll implement a stripped-down version: if a need’s deviation exceeds its urgency, spawn a task to satisfy it.
import gleam/io
pub fn evaluate(Agent(..) = agent) -> Agent {
agent.needs
|> list.fold(agent, fn(need, acc) {
if deviation(need) > need.urgency {
satisfy(need)
acc
} else {
acc
}
})
}
fn satisfy(Need(name, ..)) {
case name {
"maintenance" -> run_tests()
_ -> nil
}
}
fn run_tests() {
io.println("running tests…")
let _ = process.spawn(fn() { erlang:os_cmd("npm test") })
}
When invoked from the monitor loop, evaluate
ensures that pressing needs generate concrete behaviours.
4. Supervising behaviours on the BEAM
The Erlang runtime shines at resilience. Instead of letting a failed build crash the agent, we supervise actions and restart them if necessary. One simple approach is to use erlang:spawn_link
so the monitor receives a message when a task exits.
import gleam/erlang/process
fn run_tests() {
process.spawn_link(fn() {
case erlang:os_cmd("npm test") {
0 -> io.println("tests passed")
_ -> io.println("tests failed")
}
})
}
Linking a process gives our agent a hook for reaction: if tests fail repeatedly we can increase the urgency of a “fix” need or open an issue in the repository.
5. Persisting experience
Psi-theory learns by storing past situations and the emotions they evoked. In Gleam we can keep a lightweight event log using Erlang’s ETS tables:
import gleam/erlang/ets
pub fn record(event: String, mood: Float) {
let table = ets.new("memory", [ets.public, ets.bag])
ets.insert(table, event, mood)
}
Later cycles can query the table to adjust modulators, producing habituation or preference for strategies that worked.
6. Wiring the agent into a repo
To have the agent maintain a project, add a simple entry point in src/psi_agent.gleam
:
import psi_agent/agent
pub fn main() {
let needs = [
Need("maintenance", level: 1.0, target: 1.0, urgency: 0.3),
]
agent.new()
|> agent<-needs(needs)
|> start
}
Compile and run:
gleam run
The maintenance need will slowly drop. Once it crosses 0.3 the agent executes the test suite in a supervised process, then raises the level again in update_need
.
7. Where to go next
- Extend the set of modulators—arousal, pain, certainty—and let them influence the resolution level of planning as described in MicroPsi.
- Replace the simulated sensor with real signals from
fswatch
, CI logs or issue trackers. - Use Gleam’s type system to model the hierarchy of intentions, enabling compound plans and subgoals.
- Persist the ETS table to disk so the agent remembers experiences across restarts.
By combining psi-theory’s motivational model with Gleam’s ergonomic access to the BEAM, we get a compact foundation for software that not only edits code but cares about the state of its own work.