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

@agentiny/core

v0.8.0

Published

Lightweight TypeScript agent framework - core package

Downloads

260

Readme

@agentiny/core

Small TypeScript primitives for building reactive agents with state, triggers, conditions, and actions.

Use it when you want simple in-process automation:

  • Watch state and run actions when checks pass.
  • Chain actions by updating state.
  • Emit named events and react to them.
  • Wait for cascades to finish with settle().

Install

npm install @agentiny/core

Quick Start

import { Agent } from '@agentiny/core';

type State = {
  count: number;
  message?: string;
};

const agent = new Agent<State>({
  initialState: { count: 0 },
});

agent.when(
  (state) => state.count >= 3,
  [
    (state) => {
      state.message = 'threshold reached';
    },
  ],
);

await agent.start();
agent.updateState({ count: 3 });
await agent.settle();

console.log(agent.getState().message); // "threshold reached"
await agent.stop();

How It Works

An agent owns one state value and a set of triggers.

A trigger has:

  • check: decides if the trigger should run.
  • conditions: optional extra guards.
  • actions: functions that run when the check and conditions pass.

Checks, conditions, and actions can be sync or async.

agent.addTrigger({
  id: 'send-alert',
  check: (state) => state.count > 10,
  conditions: [(state) => state.message !== 'sent'],
  actions: [
    async (state) => {
      await sendAlert();
      state.message = 'sent';
    },
  ],
  repeat: true,
});

Actions receive the live state object. If you mutate it directly, later triggers in the same evaluation pass can see that mutation. If you need to schedule a new evaluation pass, call setState() or updateState().

Actions may also accept an optional second ActionContext argument:

async (state, ctx) => {
  // ctx.signal is aborted when the agent is stopped or paused
  await fetch(url, { signal: ctx?.signal });
};

stop() / pause() called from inside an action do not deadlock: they request shutdown and return without awaiting the execution loop.

Trigger delay is non-blocking — other triggers keep evaluating while a delayed trigger waits. Actions always see state as of execution time (not a pre-delay snapshot). At most one delay is pending per trigger.

Common Patterns

Partial State Updates

Use updateState() for object state.

agent.updateState({ count: agent.getState().count + 1 });

Use setState() when replacing the whole state value.

agent.setState({ count: 0, message: undefined });

One-Time Triggers

agent.once(
  (state) => state.count > 0,
  [(state) => console.log('first positive count', state.count)],
);

Event Triggers

agent.on('saved', [
  () => {
    console.log('saved');
  },
]);

agent.emitEvent('saved');
await agent.settle();

emitEvent is a coalesced wake signal, not a queue. Multiple emitEvent('foo') calls before the next cycle fire each matching trigger once, not once per call — if you need per-message delivery, model the count in state. A pending emission is consumed only after the trigger's conditions pass, so a failing condition leaves the emission armed until conditions are satisfied on a later cycle or the trigger is removed.

Emissions made while the agent is idle or stopped stay pending and fire on the next start(). Already-consumed emissions are not re-fired across stop/start cycles.

Wait For A Condition

waitFor(predicate, timeout?) returns a promise that resolves with the state the moment a synchronous predicate first becomes true. Where settle() waits for the whole system to go quiet, waitFor() waits for one specific condition.

await agent.start();
const ready = await agent.waitFor((state) => state.ready);
console.log(ready); // the state that satisfied the predicate

Predicates are evaluated immediately when called, on every setState()/updateState() change (so they resolve with zero latency, even while idle or paused), and on every loop cycle while running (so in-place action mutations are observed too). The resolved value is the live state reference, consistent with getState() — it is not deep cloned.

waitFor() is callable in any status, but without a running agent only setState()/updateState() changes can satisfy the predicate (trigger- and timer-driven changes require start()). It rejects with an AgentError coded WAITFOR_TIMEOUT if not satisfied within timeout (default 10000ms), or AGENT_STOPPED if the agent is stopped while waiting. A predicate that throws rejects the returned promise; invalid arguments throw synchronously.

Time-Based Triggers

every(interval, ...) fires repeatedly on a fixed interval. The interval is either milliseconds or a duration string (ms, s, m, h). Timers start when the agent is running. If a schedule is registered while idle or stopped, it begins on the next start().

agent.every('2h', [refreshFeed]);
agent.every(30_000, [hasPendingWork], [flushQueue]);
agent.every('5s', [poll], { immediate: true }); // also fires on the first cycle

at(time, ...) fires at a wall-clock time of day in the host's local timezone. Accepts "HH:MM" (24h) or "H:MMam" / "H:MMpm" (12h, case-insensitive). Repeats daily by default; pass { once: true } to fire only on the next occurrence and self-remove.

agent.at('21:30', [sendDailyReport]);
agent.at('9:30am', [isWeekday], [sendStandupReminder]);
agent.at('00:00', [resetCounters], { once: true });

Both methods accept the optional middle conditions array, return the trigger id, and honor priority / maxFires via the options bag. They throw an AgentError with code INVALID_TIME or INVALID_INTERVAL on malformed input.

Pause And Resume

Pause keeps state and triggers, but stops trigger evaluation.

await agent.start();
await agent.pause();

agent.updateState({ count: 10 }); // no triggers run while paused

await agent.resume(); // triggers are evaluated again
await agent.settle();

Call resume() to leave the paused state. Calling start() while paused throws.

Temporarily Disable A Trigger

const id = agent.when((state) => state.count > 5, [handleCount]);

agent.disableTrigger(id);
agent.enableTrigger(id);

When a trigger is re-enabled while the agent is running, it is evaluated again without requiring another state update.

Trigger Priority

Higher priority triggers run first. Equal priority triggers keep insertion order.

agent.addTrigger({
  id: 'normalize',
  priority: 100,
  check: (state) => state.count < 0,
  actions: [
    (state) => {
      state.count = 0;
    },
  ],
});

Auto-Remove After N Fires

agent.addTrigger({
  id: 'show-hint',
  check: (state) => state.count > 0,
  actions: [showHint],
  maxFires: 3,
});

maxFires must be a positive integer.

Reset State

agent.reset(); // restore initialState, keep triggers
agent.reset(true); // restore initialState, clear triggers

reset() restores the original initialState reference. It does not deep clone the initial state.

API

Agent

new Agent<TState>(config?: AgentConfig<TState>)

State

  • getState(): TState
  • setState(newState: TState): void
  • updateState(partial: Partial<TState>): void
  • subscribe(callback): () => void

Lifecycle

  • start(): Promise<void>
  • pause(): Promise<void>
  • resume(): Promise<void>
  • stop(): Promise<void>
  • reset(clearTriggers?: boolean): void
  • isRunning(): boolean
  • isPaused(): boolean
  • getStatus(): AgentStatus
  • settle(quietCycles?: number, timeout?: number): Promise<void>
  • waitFor(predicate: (state: TState) => boolean, timeout?: number): Promise<TState>

Valid lifecycle transitions:

idle -> running
running -> paused
paused -> running
running -> stopped
paused -> stopped
stopped -> running

Triggers

  • addTrigger(trigger): string
  • getTrigger(id): Trigger | undefined
  • getAllTriggers(): Trigger[]
  • removeTrigger(id): void
  • clearTriggers(): void
  • disableTrigger(id): void
  • enableTrigger(id): void
  • isTriggerDisabled(id): boolean
  • off(id): void

Convenience Methods

  • when(check, actions): string
  • when(check, conditions, actions): string
  • once(check, actions): string
  • once(check, conditions, actions): string
  • on(event, actions, repeat?): string
  • on(event, conditions, actions, repeat?): string
  • at(time, actions, options?): string
  • at(time, conditions, actions, options?): string
  • every(interval, actions, options?): string
  • every(interval, conditions, actions, options?): string
  • emitEvent(event): void
  • removeEventTrigger(event, id): void
  • removeAllEventTriggersForEvent(event): void
  • getEventTriggersForEvent(event): Trigger[]
  • getEventTriggers(): Map<string, Trigger[]>

Types

type TriggerFn<TState> = (state: TState) => boolean | Promise<boolean>;
type ConditionFn<TState> = (state: TState) => boolean | Promise<boolean>;

interface ActionContext {
  signal: AbortSignal;
  triggerId: string;
}

type ActionFn<TState> = (state: TState, ctx?: ActionContext) => void | Promise<void>;

interface AgentConfig<TState> {
  initialState?: TState;
  triggers?: Trigger<TState>[];
  onError?: (error: Error) => void;
  idleTimeout?: number;
  /** @default 1000 — breaks unbounded setState cascades */
  maxCascadeDepth?: number;
  logger?: (error: unknown) => void;
}

interface Trigger<TState> {
  id: string;
  check: TriggerFn<TState>;
  conditions?: readonly ConditionFn<TState>[];
  actions: readonly ActionFn<TState>[];
  repeat?: boolean;
  delay?: number;
  maxFires?: number;
  priority?: number;
}

enum AgentStatus {
  Idle = 'idle',
  Running = 'running',
  Paused = 'paused',
  Stopped = 'stopped',
}

Error Handling

Agent lifecycle and trigger-management errors throw AgentError.

try {
  await agent.start();
  await agent.start();
} catch (error) {
  if (error instanceof AgentError) {
    console.error(error.code, error.context);
  }
}

Use onError for errors raised while evaluating checks, conditions, or actions. Action errors are collected and reported, but later actions still run.

const agent = new Agent({
  initialState: { count: 0 },
  onError: (error) => {
    console.error(error);
  },
});

Notes

  • updateState() is a shallow merge for object state.
  • settle() requires the agent to be running.
  • waitFor() is callable in any status and resolves with the matching state.
  • Disabled triggers remain registered and can be re-enabled.
  • Event triggers created with on() are normal triggers and can be removed with off().
  • Scheduled triggers registered with at() and every() keep their trigger IDs across stop/start.
  • While paused, scheduled timers may continue ticking, but actions do not run until resume.
  • idleTimeout controls how often the loop wakes when there is no work. The default is 100ms.
  • maxCascadeDepth (default 1000) caps consecutive dirty evaluation passes. When exceeded, the agent reports CASCADE_LIMIT_EXCEEDED via onError, clears the dirty flag, and keeps running.
  • Long-running actions should observe ctx.signal for cooperative cancellation on stop/pause.

License

MIT