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.10.0

Published

Lightweight TypeScript agent framework - core package

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() and pause() abort the session signal, skip the remaining actions in the sequence, and wait for the active action and execution loop to finish. An action that ignores cancellation can keep those promises pending until it returns.

Inside a check, condition, or action, use the synchronous requestStop() or requestPause() methods. They request shutdown without waiting for the callback that invoked them:

agent.once(
  (state) => state.done,
  [
    async () => {
      await flushResults();
      agent.requestStop();
    },
  ],
);

Migration: replace await agent.stop() / await agent.pause() inside callbacks with agent.requestStop() / agent.requestPause(). Awaiting the completion methods from inside the work they wait for would deadlock. Call start() / resume() from outside those callbacks; they wait for the previous session, including one-shot and fire-count bookkeeping, before starting a new execution loop. An external stop() / pause() call can also await an existing request while that session is still finishing.

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

An event name is either a signal or a message channel. The first emitEvent that finds a listener locks it. Mixing the two throws EVENT_CHANNEL_MISMATCH. An emit with no listener does nothing and does not lock the name.

A bare call is a coalesced wake signal. Several emitEvent('saved') calls before the next cycle fire each listener once. Pass a payload to enqueue one delivery per call. emitEvent('chat', undefined) is a message whose payload is undefined, not a signal.

agent.on<{ id: string }>('saved', [
  (state, ctx) => {
    state.lastId = ctx?.payload?.id;
  },
]);

agent.emitEvent('saved'); // signal, coalesced
agent.emitEvent('saved', { id: 'a' }); // throws: name is already a signal

agent.on<{ text: string }>('chat', [
  (state, ctx) => {
    state.last = ctx?.payload?.text;
  },
]);
agent.emitEvent('chat', { text: 'hi' }); // one fire per call

ctx.payload is set on the check, the conditions, and the actions for that delivery. The property is absent for signal events and for triggers that are not event listeners.

Each listener has its own cursor into the shared queue. A failed condition leaves that listener's head in place, and later payloads stay behind it until the head is accepted or the listener is removed. Other listeners can move ahead. One queued payload is delivered per listener per pass. A listener that joins after messages are already queued starts at the end and does not replay them. There is no queue length cap: a stuck condition retains every payload behind its head.

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. Removing the last listener clears the channel, so the next listener may lock the name either way.

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>
  • requestStop(): void
  • requestPause(): 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, payload?): void
  • removeEventTrigger(event, id): void
  • removeAllEventTriggersForEvent(event): void
  • getEventTriggersForEvent(event): Trigger[]
  • getEventTriggers(): Map<string, Trigger[]>

Types

type TriggerFn<TState, TPayload = unknown> = (
  state: TState,
  ctx?: ActionContext<TPayload>,
) => boolean | Promise<boolean>;
type ConditionFn<TState, TPayload = unknown> = (
  state: TState,
  ctx?: ActionContext<TPayload>,
) => boolean | Promise<boolean>;

interface ActionContext<TPayload = unknown> {
  signal: AbortSignal;
  triggerId: string;
  /** Present only for a message-channel delivery. The value may be `undefined`. */
  payload?: TPayload;
}

type ActionFn<TState, TPayload = unknown> = (
  state: TState,
  ctx?: ActionContext<TPayload>,
) => 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.
  • settle() waits for active checks, conditions, actions, pending delays, and cascading updates, then the requested quiet cycles. It does not wait for future scheduled ticks or background work that an action did not await.
  • 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