Skip to content

Luis85/agentonomous

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

500 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agentonomous

Autonomous agent library for TypeScript simulations. Engine-agnostic, fully testable, designed to nurture an agent from birth to death in the browser with zero configuration.

Status: pre-release (0.0.0 per package.json). The Phase A MVP — a virtual-pet nurture demo — ships in examples/product-demo.

Demo: https://luis85.github.io/agentonomous/

Pre-v1 — not yet on npm. The package is not published. To evaluate locally, clone this repo and resolve agentonomous via a file: or link: dependency in your consuming project. The npm install agentonomous snippet below describes the post-publish flow.

What you get

  • An Agent class with a deterministic tick pipeline: perceive → random events → expire modifiers → decay needs → evaluate mood → reconcile animation → dispatch by control mode → run cognition → execute skills → persist + autosave.
  • Homeostatic needs (hunger, energy, …) that decay over virtual time and recover via skill invocation.
  • Buff/debuff modifiers — stackable, replace, refresh, or ignore policies; cross-cutting effects on decay, mood, skill effectiveness, intention scoring, locomotion speed, lifespan.
  • Lifecycle + mood — birth → growth → aging → death, catch-up-aware; categorical mood derived from needs + modifiers + persona; agent.kill(reason) for narrative deaths; agent.getState() surfaces everything for reactive stores.
  • Runtime time controlagent.setTimeScale(scale) changes the wall→virtual time multiplier mid-run; new scale takes effect from the next tick (determinism preserved). setTimeScale(0) freezes virtual-time progress without killing the agent. getTimeScale() reads the current value.
  • CognitionUrgencyReasoner default picks the highest-scored intention; DirectBehaviorRunner maps intentions to skill invocations; Expressive / Active / Composed needs policies.
  • Skills — typed Skill + SkillRegistry + a default bundle (feed / clean / play / rest / pet / scold / medicate + a few expressive reactions). Easy to extend with custom skills.
  • Animation state machine driven by mood + active skill + modifiers.
  • Control modes — autonomous / scripted / remote. Works as NPC, bot, or player-proxy.
  • Species-agnostic — cats, fish, birds, humans all live in the same abstraction; data-driven species descriptors via defineSpecies.
  • Persistenceagent.snapshot() + versioned schema, SnapshotStorePort with InMemory/LocalStorage/Fs adapters, auto-save policy, offline catch-up on restore.
  • Random events — seeded per-tick probability table with cooldowns.
  • Reactive store bindingbindAgentToStore(agent, listener) works with Pinia / Zustand / Redux / Svelte stores / signals.
  • Integrationsagentonomous/integrations/excalibur (Actor sync, remote controller, animation bridge).

Quickstart

npm install agentonomous
import { createAgent, defineSpecies } from 'agentonomous';

const cat = defineSpecies({
  id: 'cat',
  persona: { traits: { playfulness: 0.7 } },
  needs: [
    { id: 'hunger', level: 1, decayPerSec: 0.01 },
    { id: 'energy', level: 1, decayPerSec: 0.008 },
    { id: 'happiness', level: 0.8, decayPerSec: 0.005 },
    { id: 'health', level: 1, decayPerSec: 0.001 },
  ],
  lifecycle: {
    schedule: [
      { stage: 'kitten', atSeconds: 0 },
      { stage: 'adult', atSeconds: 120 },
      { stage: 'elder', atSeconds: 600 },
    ],
  },
});

const whiskers = createAgent({ id: 'whiskers', species: cat, timeScale: 60 });

// Player interactions flow through the bus → default skill module.
whiskers.interact('feed');

// Adjust simulation speed at any time (new scale applies next tick).
whiskers.setTimeScale(0); // pause
whiskers.setTimeScale(60); // resume at 1× (60 virtual-s per real-s)
whiskers.setTimeScale(480); // 8× fast-forward

// Game loop.
let last = performance.now();
function frame(now: number) {
  void whiskers.tick((now - last) / 1000);
  last = now;
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

That's the whole MVP surface. See examples/product-demo for a full browser demo with HUD, buffs, random events, and localStorage persistence.

Interaction flow

agent.interact(verb, params?) is the recommended entrypoint for UI- or host-triggered actions. It publishes an InteractionRequested event on the agent's bus. Reactive handlers — the defaultPetInteractionModule ships with one — translate verbs into invokeSkill(...) calls:

click → agent.interact('feed')
  └─► InteractionRequested (on bus)
      └─► defaultPetInteractionModule handler
          └─► agent.invokeSkill('feed', …)
              └─► FeedSkill.execute(ctx)
                  └─► SkillCompleted + effects (needs, modifiers)

If you route your own verbs, register a module with a reactiveHandlers entry keyed on 'InteractionRequested'. The AgentFacade passed to the handler gives you facade.invokeSkill(id, params) without reaching for the Agent directly.

Advanced: random events + custom skills

import {
  createAgent,
  defineRandomEvent,
  defineSpecies,
  err,
  ok,
  RandomEventTicker,
  SkillRegistry,
  type Skill,
} from 'agentonomous';

const rainstorm: Skill = {
  id: 'rainstorm',
  label: 'Rainstorm',
  baseEffectiveness: 1,
  execute(_params, ctx) {
    if (!ctx.hasModifier('outside')) {
      return Promise.resolve(err({ code: 'indoors', message: 'Pet is inside.' }));
    }
    ctx.applyModifier({
      id: 'wet',
      source: 'skill:rainstorm',
      appliedAt: ctx.clock.now(),
      expiresAt: ctx.clock.now() + 30_000,
      stack: 'refresh',
      effects: [{ target: { type: 'mood-bias', category: 'sad' }, kind: 'add', value: 0.2 }],
    });
    return Promise.resolve(ok({ fxHint: 'rain-drops' }));
  },
};

const skills = new SkillRegistry();
skills.register(rainstorm);

const randomEvents = new RandomEventTicker([
  defineRandomEvent({
    id: 'weather:rain',
    probabilityPerSecond: 0.005,
    cooldownSeconds: 120,
    emit: () => ({ type: 'RandomEvent', subtype: 'rain', at: 0 }),
  }),
]);

const pet = createAgent({
  id: 'whiskers',
  species: defineSpecies({ id: 'cat' }),
  skills,
  randomEvents,
});

Skills return ok(...) for success or err(...) for expected failure. A thrown exception is caught by the tick pipeline and surfaced as SkillFailed with code: 'execution-threw' — no RNG draws happen between the throw and the next tick, so replay stays deterministic.

Running the example

The examples/product-demo demo resolves agentonomous (and its cognition/adapters/* subpaths) via Vite + tsconfig aliases that point at the library's built dist/ — not via an npm dependency. That keeps the demo import shape identical to a real consumer while sidestepping npm's self-nested-junction failure on Windows when a file:../.. dep points at its own ancestor. You must build the library before the example resolves it:

# From the repo root.
npm install
npm run build                       # → dist/ populated so the example can import

# Install the example's own deps + start Vite dev server.
cd examples/product-demo
npm install
npm run dev

Open the printed http://localhost:5173/ URL. Feed, pet, clean, and watch the pet grow up, get hungry, and eventually die (with a life-summary modal and a "New pet" button). LocalStorage persists the pet across reloads. The HUD includes a speed picker (Pause / 0.5× / 1× / 2× / 4× / 8× — also persisted) and a Reset button (confirm-gated) for a fresh start.

Determinism

Under a fixed SeededRng + ManualClock, every tick produces a byte-identical DecisionTrace. Tests assert this directly:

const runA = await runScriptedReplay();
const runB = await runScriptedReplay();
expect(runA.traces).toEqual(runB.traces);
expect(runA.events).toEqual(runB.events);
expect(runA.finalState).toEqual(runB.finalState);

The library forbids raw Date.now() / Math.random() / setTimeout inside its own code via ESLint rules — all non-determinism flows through the WallClock, Rng, and RemoteController ports.

Adding your own species

Species are pure data. Drop a JSON file under species/ (schema: schema/species.schema.json), load it, and pass the result to createAgent:

import catJson from './species/cat.species.json' with { type: 'json' };
import { defineSpecies, createAgent } from 'agentonomous';

const cat = defineSpecies(catJson);
const pet = createAgent({ id: 'whiskers', species: cat });

Any species descriptor can declare: needs, lifecycle, persona, appearance, locomotion, passiveModifiers, allowedSkills, dialogueCapable. Explicit config on createAgent overrides descriptor defaults.

Reactive store (Pinia, Zustand, …)

bindAgentToStore takes any listener; wire it into whichever reactive store you use. A Pinia example end-to-end:

// stores/pet.ts
import { defineStore } from 'pinia';
import type { AgentState } from 'agentonomous';

export const usePetStore = defineStore('pet', {
  state: (): { snapshot: AgentState | null } => ({ snapshot: null }),
  actions: {
    syncFromAgent(state: AgentState): void {
      this.snapshot = state;
    },
  },
});
// main.ts
import { bindAgentToStore, createAgent, defineSpecies } from 'agentonomous';
import { usePetStore } from './stores/pet';

const pet = createAgent({ id: 'whiskers', species: defineSpecies({ id: 'cat' }) });
const store = usePetStore();

const unsubscribe = bindAgentToStore(pet, (state) => {
  store.syncFromAgent(state);
});

The listener fires synchronously on every event and receives the current getState() slice (id, stage, needs, modifiers, mood, animation, halted, ageSeconds). Call unsubscribe() to detach.

LLM provider port (preview)

Agent reasoning can be backed by an LLM via the LlmProviderPort contract. v1.0 ships the completion-only surface (one complete(messages, opts) call → one LlmCompletion); streaming + tool-use land in Phase B as additive methods on the same port — existing adapters keep working unchanged.

The library ships MockLlmProvider for deterministic playback in tests and golden-trace replays. Concrete AnthropicLlmProvider / OpenAiLlmProvider adapters are deferred to Phase B; for now consumers either wrap their own provider against the port or run against the mock.

import { MockLlmProvider, type LlmProviderPort } from 'agentonomous';

const provider: LlmProviderPort = new MockLlmProvider({
  defaultModel: 'mock-llm-1',
  scripts: [{ text: 'feed' }, { text: 'rest' }, { text: 'noop' }],
});

const completion = await provider.complete([
  { role: 'system', content: 'You are a pet care assistant. Reply with one verb.' },
  { role: 'user', content: 'What should the pet do next?' },
]);
// completion.text === 'feed'

A full end-to-end runnable example — MockLlmProviderLlmReasonercreateAgent under SeededRng + ManualClock, asserting byte-identical traces across two runs — lives in examples/llm-mock/.

Development

nvm use               # node 22
npm install
npm test              # vitest
npm run typecheck     # tsc --noEmit
npm run lint          # eslint 9 flat config
npm run build         # vite library mode → dist/
npm run docs          # typedoc → docs/
npm run analyze       # build + list the 20 largest dist/*.js files by bytes

Bundle-size budget

The library's core bundle and each adapter subpath have a per-entry size budget enforced via size-limit (see the size-limit field in package.json for the current caps; CI rejects regressions). The agentonomous/integrations/excalibur subpath is a separate entry so consumers who don't use Excalibur don't pay for it. Run npm run analyze after meaningful changes; a significant regression is a signal to check for accidentally-bundled adapters or heavy deps.

Phase A (the virtual-pet nurture MVP) is feature-complete and in the pre-1.0 polish + harden pass — see docs/plans/2026-04-25-comprehensive-polish-and-harden.md. Phase B (sim-ecs adapter, LLM tool-use, Markdown memory, social / dialogue, possession / jobs, Mistreevous BTs, JS-son BDI, expanded tfjs learning) lands post-1.0.

Contributing

See CONTRIBUTING.md for branch model, commit style, PR conventions, and the release flow. TL;DR: work on a topic branch cut from develop, PR against develop, keep commits small and reversible.

License

MIT — see LICENSE.

About

Autonomous agent library for TypeScript simulations.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors