JSPM

@refix/proactivity

0.2.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 76
  • Score
    100M100P100Q54586F
  • License Apache-2.0

Proactivity primitives for autonomous agents โ€” scheduling, governance, goals, briefing

Package Exports

  • @refix/proactivity
  • @refix/proactivity/anthropic
  • @refix/proactivity/bullmq
  • @refix/proactivity/eve
  • @refix/proactivity/langgraph
  • @refix/proactivity/postgres
  • @refix/proactivity/prompts
  • @refix/proactivity/timer

Readme

๐Ÿชฉ Proactivity

Make your reactive agents proactive

LangGraph   Vercel AI SDK   OpenAI SDK   Anthropic SDK   Mastra   Eve

The TypeScript SDK for proactive agents: durable wake scheduling, cross-wake goal memory, LLM-driven cadence, and idempotent action governance.

Works with LangGraph, the Anthropic SDK, Eve, OpenClaw, and Hermes, or any loop you own.

The problem

LangGraph, CrewAI, and friends give you a reasoning loop: you call it, it thinks, it returns. That's a reactive agent. A proactive one wakes on its own, notices what changed, pursues goals across wakes, and sets its own pace.

The moment an agent runs unsupervised, you need guardrails or you rebuild them one incident at a time: idempotency after the first double-post, rate caps after the first runaway loop, crash recovery after the first lost job, an audit trail after the first "what did it do?"

This gives you both: the wake loop and the envelope.

Quick start

pnpm add @refix/proactivity

1. The agent you have

A ReAct agent that can read GitHub and post to Slack. Reactive: it runs when you call it, answers, and stops existing.

const agent = createReactAgent({
  llm,
  tools: [listIssues, listPullRequests, postToSlack],
  prompt: "You watch a GitHub repo and keep #eng informed.",
});

2. Make it proactive

The agent's reasoning, tools, and prompt don't change. One structural touch: the outbound tool goes through governed(), so it can never double-post.

import { proactive } from "@refix/proactivity";
import { fromLangGraph, governed, langchainModel } from "@refix/proactivity/langgraph";

const agent = createReactAgent({
  llm,
  tools: [listIssues, listPullRequests, governed(postToSlack)], // โ† the one change
  prompt: "You watch a GitHub repo and keep #eng informed.",
});

const handle = proactive(fromLangGraph(agent), {
  // Your own LLM โ€” it powers the SDK's reflection step (bookkeeping + pacing).
  reflection: { model: langchainModel(llm) },
  goals: [
    {
      title: "Keep #eng on top of acme/api",
      objective:
        "Each wake, check issues and PRs against what previous wakes reported. " +
        "Post to #eng when something needs a human: a new bug report, a PR stale " +
        "for 3+ days, a thread going in circles. Stay silent otherwise.",
      doneCondition: "Standing goal โ€” never done; keep watching.",
      pinned: true, // the model can't close this goal โ€” only you can
    },
  ],
  cadence: { min: "15m", max: "24h" }, // it picks its own pace inside this window
});

await handle.start("acme/api"); // that's it โ€” first wake in 15 minutes

No store configured means in-memory; hand it Postgres when you deploy (below). handle.wake("acme/api") wakes it right now (webhooks enter here).

3. Watch it run

Two wakes, four hours apart:

[proactive:acme/api] wake #1 (scheduled) โ€” 1 goal
[proactive:acme/api] โš™ list_issues {"repo":"acme/api"}
[proactive:acme/api] ๐Ÿ’ญ Nothing changed since the last look. No post warranted.
[proactive:acme/api] โœŽ quiet repo, 14 open issues, nothing new โ€” next wake in 4h
[proactive:acme/api] wake #1 done โ€” next wake at 6:12 PM

        ยทยทยทยทยท four hours later ยทยทยทยทยท

[proactive:acme/api] wake #2 (scheduled) โ€” 1 goal, last wake 4h ago
[proactive:acme/api] โš™ list_issues {"repo":"acme/api"}
[proactive:acme/api] ๐Ÿ’ญ Two new bug reports, one looks P0. #eng should know.
[proactive:acme/api] โœ” post_to_slack โ€” taken
[proactive:acme/api] โœŽ escalated 2 new bugs to #eng โ€” next wake in 30m (watch for movement)
[proactive:acme/api] wake #2 done (acted) โ€” next wake at 6:47 PM

Wake 1: nothing new, so reflection backed off toward the 24h ceiling. Wake 2: two bug reports, one P0 โ€” the agent decided to escalate, posted to #eng, updated its scratchpad, and tightened to 30m to watch for follow-up.

[!TIP] Route the event stream to your own logger with observe: (event) => โ€ฆ, or silence it with observe: false.

What you get

  • Your agent stays a black box. It gets briefed before it runs, observed while it runs, and reflected on after. No restructuring, no framework migration.
  • Memory across wakes. Every wake opens with a situation report: standing goals with their living scratchpads, recent wakes, actions already taken. With report: { summarizeOlderWakes: true }, history that ages out folds into a rolling summary โ€” wake #500 still knows what wake #3 promised.
  • Self-set cadence. After each wake, reflection picks the next one between your min and max: sooner while things are changing, backed off when quiet. handle.wake() is the event-driven entry.
  • Governed actions: idempotent, capped, audited. Wrap a tool with governed() and it gets an idempotency key claimed before the side effect, a per-wake action cap, and an audit row for every attempt. Whether a wake "acted" is derived from the audit trail, not from what the model claims. Denials go back to the model so it replans instead of retrying blindly.
  • Reflection on your own model. The SDK's reasoning step runs on the LLM client you already have (same keys, same retries, same tracing). No second provider.
  • Goals are an API. handle.addGoal() when your user clicks "watch this", handle.completeGoal() when they stop caring. Reflection can evolve a pinned goal's scratchpad but can never close it.
  • Durable. Postgres store and BullMQ scheduler for production; handle.resume() re-arms every loop after a restart.

Works with your stack

LangGraph. fromLangGraph(graph) wraps any compiled graph; transcript is recorded via callbacks, subgraphs included.

Anthropic SDK. You keep your hand-rolled loop. fromAnthropic() hands it a traced client with an identical surface, so the SDK reconstructs the transcript without touching the loop (anthropicLoop() if you'd rather the SDK own the loop):

import { proactive } from "@refix/proactivity";
import { anthropicModel, fromAnthropic } from "@refix/proactivity/anthropic";

const handle = proactive(
  fromAnthropic({
    client: anthropic,
    // Your existing call โ†’ execute tools โ†’ feed results back loop. It opens
    // with the injected situation report where a hardcoded request used to be.
    run: ({ client, message }) => briefingLoop(client, tools, message),
  }),
  {
    reflection: { model: anthropicModel(anthropic, "claude-sonnet-5") },
    goals: [/* same as above */],
    cadence: { min: "15m", max: "24h" },
  },
);

A hand-rolled loop has no tool object to wrap, so govern the side effect directly: governedPerform({ actionType, target, perform }) is the same envelope without the wrapper.

Eve. createEveProactivity() from @refix/proactivity/eve is built with Eve's grain: hooks, a due-gate over Eve's static cron, and a terminal finish_heartbeat tool.

OpenClaw / Hermes โ€” ship as plugins. Paste this into your agent:

Install and enable the proactivity plugin: run `openclaw plugins install npm:@refix/proactivity-openclaw`, then `openclaw plugins enable proactivity`. Confirm both commands succeeded. If either fails, follow https://github.com/refixai/proactivity/tree/main/integrations/openclaw.
Install and enable the proactivity plugin: run `pip install proactivity-hermes`, then `hermes plugins enable proactivity`. Confirm both commands succeeded. If either fails, follow https://github.com/refixai/proactivity/tree/main/integrations/hermes.

Both add the governance envelope, durable goals, and a set_cadence tool to your existing agent. Config and honest limitations: integrations/openclaw, integrations/hermes.

Anything else. An adapter is { name, run(input) => Promise<Transcript> }. Three concerns, nothing else.

How a wake runs

Every wake runs four moments:

  1. Inject: a situation report built from the store (goals + their scratchpads, recent wakes, actions already taken) arrives as the agent's message. Shape it or replace it with the agentInput callback; statefulness is your dial.
  2. Run: your unchanged agent executes. Tools you wrapped with governed() pass through the governance envelope; everything else runs untouched. Outside a wake, governed tools are transparent passthroughs.
  3. Reflect: one structured-output call on your model reads the full transcript, writes the ledger entry, evolves each goal's scratchpad, and picks the next wake time. Always on; a failed reflection degrades to safe defaults instead of failing the wake.
  4. Schedule: the scheduler re-arms at the reflected cadence.

Production

The in-memory store is for development. In production the store holds everything โ€” goals, ledger, audit trail, schedule โ€” so the process is disposable:

pnpm add pg bullmq   # optional peers for /postgres and /bullmq
import { proactive } from "@refix/proactivity";
import { createPostgresStore } from "@refix/proactivity/postgres";
import { createBullMQAdapter } from "@refix/proactivity/bullmq";

const store = createPostgresStore({ connectionString: process.env.DATABASE_URL });
await store.migrate(); // idempotent; safe to run on every boot

const handle = proactive(adapter, {
  reflection: { model },
  goals,
  store,
  schedule: createBullMQAdapter({ queueName: "wakes", connection: { host: "localhost", port: 6379 } }),
});

await handle.resume(); // after a restart: re-arm every entity that should be running

migrate() creates proactivity_*-prefixed tables alongside your own; createPostgresStore also takes a pg.Pool you already manage.

Overrides

The defaults are opinionated so the quick start stays short. Each layer can be replaced:

Override How
What your agent sees each wake agentInput: (ctx) => โ€ฆ โ€” goals, ledger, and rolling summary in; any input shape out. The rendered report stays available as ctx.report.
Whether a wake happens at all shouldWake: (ctx) => boolean โ€” the only pre-model gate. Cost control only; judgment stays with the agent.
Reflection's guidance reflection.instructions โ€” product-specific guidance appended to the goals / scheduling / ledger sections of the prompt.
Reflection's prompt reflection.prompt: (ctx) => string โ€” full prompt takeover; the output schema stays enforced.
Reflection itself reflection.run: (ctx) => Promise<unknown> โ€” replace the single call with anything: a sub-agent over the store, a deep pass every Nth wake. Output is still validated and clamped.
Report depth report.recentWakes (default 5), report.summarizeOlderWakes (default off).
Action ceiling governance.maxActionsPerWake (default 10).
Logging and errors observe (a function routes the event stream, false silences it), onError.
Storage and scheduling store (any ProactivityStore), schedule (any SchedulerAdapter).

A few things are deliberately not configurable: reflection always runs, idempotency is claimed before the side effect, pinned can never be set by model output, and observers can never fail a wake.

Under the hood

proactive() is compiled from documented, individually usable primitives (a scheduler, a heartbeat, a goal store, and the governance envelope) that share one store. When the wrapper stops fitting your flow, dropping one layer down isn't a migration: same tables, same ledger, same scheduler. The primitives layer, plan/act mode, and per-framework wiring patterns are documented in PRIMITIVES.md, with runnable, compile-checked examples under examples/.

License

Apache-2.0. See LICENSE.