@dynamicagents/plugins
v0.12.0
Published
Optional, composable capabilities for a Dynamic Agent: ARC-AGI-3, browser rendering, a Linux container, git and pull requests, workspace, episodic recall, and pre-turn triage. One subpath per plugin, so a bundle grows only with what it imports.
Downloads
747
Maintainers
Readme
@dynamicagents/plugins
Optional, composable capabilities for a Dynamic Agent.
One subpath per plugin, one factory per subpath, config passed at instantiation. Your bundle grows only with what you import.
npm install @dynamicagents/pluginsPart of a three-package split:
@dynamicagents/core(the mandatory foundation) ·@dynamicagents/plugins(this) ·starter(a working agent that composes them).
The one file you edit
// src/plugins.ts
import { arcAgi } from "@dynamicagents/plugins/arc-agi";
import { browser } from "@dynamicagents/plugins/browser";
import { recall } from "@dynamicagents/plugins/recall";
export interface PluginHost {
env: Env;
storage: DurableObjectStorage;
/** The verified caller this Durable Object belongs to. See below. */
callerKey: () => string;
}
export const plugins = ({ env, storage, callerKey }: PluginHost) => [
arcAgi({ apiKey: env.ARC_API_KEY, storage }),
browser({ binding: env.BROWSER }),
recall({ ai: env.AI, index: env.VECTORIZE, namespace: callerKey })
];Delete a line and that module leaves your bundle entirely. Nothing in core imports a plugin,
and there is no root barrel — @dynamicagents/plugins on its own does not resolve — so the
guarantee is structural rather than a tree-shaker's opinion. npm run verify:exports asserts
it on the built graph before every publish.
plugins is a function, not a module-level array: on Workers env does not exist at module
scope, and core's registry is built per Durable Object instance in onStart().
export class MyAgent extends Agent<Env> {
/** Set on the first verified request; constant thereafter. See below. */
private identity?: string;
async onStart() {
this.runtime = createAgentRuntime({
config,
plugins: plugins({
env: this.env,
storage: this.ctx.storage,
// A thunk, not a value: `onStart` runs before any request, so the caller
// is not known yet. The DO is keyed 1:1 by that caller, so it is constant
// once it is — this just defers reading it until it exists.
callerKey: () => this.identity ?? ""
}),
env: this.env // verify every plugin's declared bindings exist, at startup
});
}
async onTurn(turn: AgentTurn, identity: GatekeeperIdentity) {
this.identity ??= identity.key!;
// …
}
}That deferral is the whole reason /recall takes namespace as a function. Anything else
needing per-caller state takes it the same way.
The plugins
| Subpath | What it adds | Needs |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| /arc-agi | Play ARC-AGI-3 games — a delegable subtask type, a catalogue tool, a scorecard ledger | ARC_API_KEY |
| /browser | Read web pages via Browser Rendering Quick Actions | BROWSER (paid plan) |
| /claude-code | Delegate a coding task to a Claude Code session in the workspace container | one or more claude setup-token credentials |
| /computer | A Linux container whose filesystem outlives it — shell, package manager, network, and the Durable Object it runs in | @cloudflare/computer, @platformatic/vfs (paid) |
| /recall | Episodic memory over Vectorize — search history that compaction folded away | VECTORIZE (1024-dim/cosine) |
| /repo | Clone, commit, push a branch, open a pull request — over any container | GITHUB_TOKEN |
| /scratch | A throwaway git repository with no remote, for work that needs a container but no checkout | — |
| /triage | A pre-turn gate: is this message even for me? | — |
| /workspace | A durable file store for long subagent runs, plus tools over it | @cloudflare/shell |
Each directory has its own README with the config shape and a paste-ready wrangler.jsonc
snippet — a plugin cannot add its own binding, which is why it declares what it needs.
Writing one
import { definePlugin, type AgentPlugin } from "@dynamicagents/core";
export function scraper(config: { apiKey: string }): AgentPlugin {
return definePlugin({
key: "scraper",
mainAgentTools: () => ({ fetchPage: /* … */ }),
capability: "You can fetch and summarize a page.",
requires: { secrets: ["SCRAPER_API_KEY"] }
});
}Three rules the whole design rests on:
- Never name a consumer's
Env. It is an ambient interfacewrangler typesgenerates into their app. Take bindings and secrets as config, which is also the only thing that works on Workers, whereenvhas no module scope. definePluginsetscontractVersionfrom the core you compiled against. Never write that number as a literal — the point is that it moves, so a version train that leaves one repo behind fails at startup with a sentence instead of a structural-type error.- Declare a capability block in exactly one place. If your plugin has a
subtaskType, put it there; otherwise on the plugin. Both are rendered, by different call sites.
Testing
Specs run inside real workerd via @cloudflare/vitest-plugin, with the harness from
@dynamicagents/core/testing.
npm test # the whole suite, no credentials and no network
npm run check # prettier + eslint + tsc + build, then verify:exports over itWorking against an unpublished core
A plain npm install resolves @dynamicagents/core from whatever this branch's
package.json declares — AGENTS.md says which — and does it right over a local build
you are testing against.
npm run link:local # npm pack + tarball install from ../coreRun it after changing core, and after any npm install here, which silently
undoes it. Skipping it is quiet and misleading: tsc reports errors in this
repo's source for a contract change sitting uninstalled one directory away.
npm pack + tarball, deliberately — not npm link, which symlinks the checkout and
gives it its own copy of every peer. Two copies of agents in one bundle breaks the
Session types and every instanceof, at runtime rather than at the type level.
Nothing is written to package.json, so CI never builds against a local checkout.
test/arc-agi/recorded.spec.ts drives the real ARC API and replays a committed
cassette, so it needs no key either. Re-record it against the live API with:
npm run test:record # real ARC_API_KEY in .env.test (see .env.test.example)The key reaches the live ARC API and nothing else: the recorder excludes the auth header, so
it never lands in the committed cassette. It reaches the spec only as a Miniflare binding —
.env.test is loaded into Node's process.env, which a spec running in workerd cannot
see, so vitest.config.ts hands it across explicitly.
License
GPL-3.0-only
