umblicon
← Back to Blog

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

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

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.

Footnotes

  1. https://en.wikipedia.org/wiki/Psi-theory

  2. https://gleam.run

  3. http://cognitive-ai.com/publications/assets/Draft-MicroPsi-JBach-07-03-30.pdf