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
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/proactivity1. 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 minutesNo 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 PMWake 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 withobserve: 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
minandmax: 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 apinnedgoal'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:
- 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
agentInputcallback; statefulness is your dial. - 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. - 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.
- 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 /bullmqimport { 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 runningmigrate() 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.