AI personas use attention, emotion, and memory to select when they send messages.
Perfectman is a social simulation with AI personas in shared chat channels. Agents do not take fixed turns. Each agent uses an internal model to select an action or remain silent.
The model uses event visibility, attention, interpretation, motivation, emotion, pressure, and inhibition. An agent sends a message only when its pressure is sufficient to overcome its inhibition.
This model can produce delayed messages, private channels, and inferred exclusion. These behaviors result from the model. The system does not use a script for each social interaction.
- Action selection: A ten-stage pipeline selects an action. The agent can remain silent.
- Inferred exclusion: An agent can infer exclusion from the absence of public messages, without an explicit notification.
- Private channels: Agents can create private channels through curiosity, gossip, jealousy, secrecy, or relationship repair.
- Biased memory: Emotion can change the episodic memory and relationship memory. These memories are not exact event records.
- Behavior tests: The reported results cover 123 deterministic scenarios without a model connection. CI requires all behavioral signals to pass.
Note
Status: This is an early experiment before version 1.0. The event-oriented runtime and social presence engine operate together. Work continues on exclusion, masking, grudges, and biased memory. Errors and limitations remain.
- What this is
- Quickstart
- Watch a run in the browser
- Turn a saved run into video
- Running with a real model, for free
- Bring your own personas
- Benchmarks
- Architecture
- Status / open questions
- Releases
- Contributing
- License
Each agent action goes through this pipeline:
event → visibility → attention → interpretation → motivation
→ emotion → pressure → inhibition → intent → resolver → committed event
Any stage can stop an action. Silence is a valid result.
The pipeline supports these behaviors:
| Behavior | What it looks like |
|---|---|
| Selective attention | An agent can select a message for attention and then send a message or remain silent. |
| Private alliance | An agent can create or enter a private channel through curiosity, gossip, jealousy, secrecy, or relationship repair. |
| Inferred exclusion | An agent can infer exclusion from the absence of public messages. |
| Meaningful lateness | A delay can change the meaning of a message. |
| Biased memory | Emotion can change the memory of an event. |
A rule-based spectator layer uses the committed event log to give a narrative summary of social changes. This summary does not contain backend metrics.
The application requires Node.js and pnpm.
Install the dependencies. Then build the application:
pnpm install
pnpm buildCreate the local configuration directories. Then copy the example configuration:
Git ignores these local configuration paths to protect persona data.
mkdir -p config/personas config/persona-notes
cp examples/simulations/mock.inline-personas.example.json config/index.jsonStart the simulation:
pnpm --filter @perfectman/server simulationThe default configuration uses providerType: "mock". This simulation does not
require an API key or a model.
The CLI writes a static HTML file after the run stops. The web runner takes markdown instead of JSON and streams the run while it happens:
pnpm build
pnpm webOpen http://localhost:4317, drop in a scenario and its personas, and press
Run — or pick a ready-made cast and scene from examples/presets/.
The form compiles the markdown on every change and shows the resulting config
before anything runs — cast, channels, detected language, and which canonical
persona each agent inherited its engine calibration from. Agent thinking and
per-pulse emotion are operator events with no table behind them, so each run
also writes out/runs/<id>/replay.json, which is the only place they survive.
For UI work, pnpm web:dev serves the interface with hot reload on port 5317
and proxies the API to the run server.
Use an existing transcript, replay, or script to create a local MP4 and an interactive story viewer with channels, private conversations, and music:
pnpm video --input docs/eval/evidence/deepseek/scenarios/v1_exclusion_inferred.json --out out/exclusion.mp4The command keeps source order and recorded emotion data. It requires FFmpeg and a completed build. See video inputs and usage.
These options do not require a paid API key.
Native Ollama — recommended on macOS / Apple Silicon
Docker Desktop does not support GPU passthrough on macOS. The reported speed is approximately 1.2–1.5 tokens per second with Docker on the CPU. The reported speed with native Metal is approximately 40 tokens per second.
Install Ollama directly on macOS. Then download the model. Start the server:
brew install ollama # or download from https://ollama.com
ollama pull qwen3:1.7b
ollama serve # API on :11434Copy examples/simulations/qwen3-local.example.json to config/index.json.
Local Qwen3 via Docker — Linux with an NVIDIA GPU
pnpm qwen:dev # pulls qwen3:1.7b, portable CPU-safe default
# or: pnpm qwen:dev:8b for the 8B modelIf your machine has an NVIDIA driver, enable GPU passthrough with this command:
docker compose -f docker/qwen3/qwen3.compose.yml \
-f docker/qwen3/qwen3.gpu.compose.yml up -dFreeLLMAPI — one proxy for free-tier provider keys
cp .env.example .env # set FREELLMAPI_ENCRYPTION_KEY
pnpm freellm:dev # API on :3001, dashboard on :5173Add the provider keys in the dashboard. Create a unified key in the dashboard.
Set FREELLMAPI_KEY in .env.
Simulation in Docker — local Node.js and pnpm are not necessary
mkdir -p config
cp examples/simulations/mock.inline-personas.example.json config/index.json
docker compose -f docker/app/app.compose.yml up --buildThe first Compose file determines the base directory for relative paths. Keep
docker/app/app.compose.yml first so that these paths use docker/app/.
If Ollama runs in the same Compose project, set the provider baseURL to
http://qwen3:11434/v1. This address uses container DNS. A localhost address
refers to the simulation container itself.
Start the services. Then display the simulation log:
docker compose -f docker/app/app.compose.yml \
-f docker/qwen3/qwen3.compose.yml up -d
docker compose -f docker/app/app.compose.yml \
-f docker/qwen3/qwen3.compose.yml logs -f simulationFor a real provider, give the container read-only access to the .env file that
the CLI uses:
docker compose -f docker/app/app.compose.yml run --rm \
-v "$PWD/.env:/app/.env:ro" simulationSee docs/personas/README.md for the persona interview
and compilation procedure. The local persona paths under config/ and
docs/personas/ contain person-specific files. Git ignores these paths. Generic
instructions and templates remain in the repository.
The benchmark suite tests the behaviors above. It uses a mock provider and a rule judge. It does not require an API key or a model server.
Run the benchmark suite. Then check the CI acceptance criteria:
pnpm --filter @perfectman/eval bench --mode mock --judge rule --out out/bench.json
node scripts/ci/check-bench-gate.mjs out/bench.jsonReported scenario results: 123 deterministic scenario runs cover five behavioral categories. These figures describe the existing benchmark report. They are not results from a new test run.
| Category | Runs | Signal pass rate |
|---|---|---|
motive_archetype |
51 | 100% |
v1_behavior |
27 | 100% |
stagnation_attractor |
18 | 100% |
calibration |
15 | 100% |
edge_chaos |
12 | 100% |
| Total | 123 | 100% |
Reported behavioral signals: 276 assertions check the actions of the social engine. These assertions do not assess the quality of generated text.
| Signal | Passed | Rate |
|---|---|---|
no_llm_failures |
123 / 123 | 100% |
event_committed |
75 / 75 | 100% |
private_channel_created |
48 / 48 | 100% |
emotion_rises |
15 / 15 | 100% |
emotion_stays |
15 / 15 | 100% |
No scenario runs failed in the report. The mean result for runtime probes
is 91.0%. The lowest results are content-repetition (21.1%) and
memory-write (70.7%). CI records these results for comparison but does not use
them as acceptance criteria.
| Layer | Result | Gated in CI? |
|---|---|---|
| Behavioral signals | 100% | Yes. All signals must pass. |
| Runtime probes | 91.0% | No. CI records these results for comparison. |
| Judge axis scores | 6 of 8 targets met | No. These scores are advisory. |
Important
CI does not use judge axis scores as acceptance criteria. The rule judge has not met its calibration target. Cohen's κ against the golden-labeled set is 0.219 against a target of 0.7 (n=39). These scores permit comparisons, but they are not evidence of qualitative accuracy.
The reported scores for in_character (2.99 / 4.0) and voice_match
(1.16 / 3.8) are below target. The largest disagreement with the golden labels
occurs on voice_match.
All reported deterministic behavioral signals passed. The qualitative scores are not sufficiently reliable. Judge calibration remains open work.
The reported unit and hygiene suite contains 1,426 tests across 126 files. The recorded run passed on Node 22.
Check the types. Then run the unit tests and hygiene checks:
pnpm lint # typecheck, all four packages
pnpm test:all # unit tests + hygiene gatesThe full design is in docs/README.md. It contains the
authoritative architecture, the emotion model, the social presence engine, and
open design questions.
The main layers have these responsibilities:
| Layer | Responsibility |
|---|---|
Event-oriented runtime (packages/server) |
It contains the append-only event log, command handlers, and intent resolver. It creates delivery, spectator, operator, and engine snapshot projections. It supports Socket.IO, Discord, stdout, and mock transports. |
Social presence engine (packages/engine) |
It calculates attention, motivation, emotion, pressure, and inhibition without I/O. |
Agent mind (packages/server/src/agent) |
It contains persona identity, writing style, and relationship beliefs. Its LLM runtime converts perception into intent. |
Evaluation harness (packages/eval) |
It contains the scenario registry, rule and LLM judges, probes, calibration, and narration. |
| Continuity system | It contains episodic memory, relationship memory, emotional drift, and rumination. Emotion can change the memory. |
This is an active experiment. It is not a completed product.
The open design questions include:
- What can spectators see from private channels?
- How much scoring logic does the system need?
- Which symbolic actions must the system keep?
- How can judge calibration meet its target?
Contributors can start with these questions.
For each tagged release (v*), docker-release.yml publishes a runtime container
image to GHCR at ghcr.io/caiotheodoro/perfectman. The image tag omits the initial
v. For example, release v0.1.0 produces image tag 0.1.0. Releases that are
not prereleases also update latest.
Download the latest release image:
docker pull ghcr.io/caiotheodoro/perfectman:latestFor reproducible runs, use a release-specific image tag instead of latest.
See CONTRIBUTING.md. Issues with the good first issue label
have a limited scope. You do not need to read the full architecture document to
start these issues.
