@agentaily/agent-loop
v0.1.0
Published
A minimal, runtime-agnostic agent loop framework with first-class skills, memory, and sessions. Edge-ready (Cloudflare Workers), zero runtime dependencies.
Maintainers
Readme
@agentaily/agent-loop
A minimal, runtime-agnostic agent loop with first-class skills, memory, and sessions.
- Tiny & zero runtime deps — core is a few hundred lines; only uses
fetch. - Edge-ready — runs on Cloudflare Workers, Node 18+, Deno, Bun, and browsers.
- Provider-agnostic — ships an OpenAI-compatible adapter (works with DeepSeek); bring your own.
- Pluggable storage — in-memory by default; a Cloudflare KV adapter included.
Built to power the client/edge agent loops behind agentaily's "chat × everything" products (first consumer: the 2bti worker).
Install
npm i @agentaily/agent-loopQuick start
import { Agent, defineTool } from '@agentaily/agent-loop'
import { deepseek } from '@agentaily/agent-loop/providers'
const getWeather = defineTool({
name: 'get_weather',
description: 'Get the current weather for a city',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
handler: (args) => ({ city: args.city, tempC: 21, sky: 'clear' }),
})
const agent = new Agent({
provider: deepseek({ apiKey: process.env.DEEPSEEK_KEY! }),
instructions: 'You are a concise, friendly assistant.',
tools: [getWeather],
})
const res = await agent.run('what is the weather in Tokyo?')
console.log(res.text)The loop
agent.run(message) does exactly what you'd hand-write:
user message ─▶ call LLM (with tools + skill index + memory index)
│
tool calls? ──no──▶ final answer ✔ (session saved)
│yes
run each tool ─▶ append results ─▶ loop (up to maxSteps)run() returns { text, session, steps, stoppedOnMaxSteps }. Every step is
observable via the onStep callback.
Skills — progressive disclosure
A skill is a named, markdown-described capability. The model only sees each
skill's name: description in the system prompt; it pulls the full instructions
in on demand via the built-in load_skill tool. Skills may carry their own
tools, which become available only after the skill is loaded.
import { parseSkill } from '@agentaily/agent-loop'
const refunds = parseSkill(`---
name: refunds
description: process customer refunds
---
To refund an order, call issue_refund with the order id, then confirm to the user.`)
const agent = new Agent({ provider, skills: [refunds], tools: [/* ... */] })You can also pass plain Skill objects ({ name, description, instructions, tools? })
or a SkillRegistry.
Memory — durable facts across sessions
A MemoryStore holds facts that outlive a single conversation. The built-in
remember / recall tools let the agent write and search it, and a compact
index of what's remembered is injected into every system prompt.
import { InMemoryMemoryStore } from '@agentaily/agent-loop'
const memory = new InMemoryMemoryStore()
const agent = new Agent({ provider, memory })
// the model can now call remember({key, value}) and recall({query})Sessions — multi-turn conversations
A SessionStore persists conversation history. Resume by passing sessionId:
const first = await agent.run('my name is Sam')
await agent.run({ message: 'what is my name?', sessionId: first.session.id })Default is in-memory. On Cloudflare Workers, persist to KV:
import { KVSessionStore, KVMemoryStore } from '@agentaily/agent-loop/adapters/cf-kv'
const agent = new Agent({
provider,
sessions: new KVSessionStore(env.AGENT_KV),
memory: new KVMemoryStore(env.AGENT_KV),
})See examples/cf-worker for a complete Worker endpoint.
API surface
| Export | What |
| --- | --- |
| Agent | the loop; new Agent(opts).run(input) |
| defineTool | build a { name, description, parameters, handler } tool |
| SkillRegistry, parseSkill | manage / parse markdown skills |
| InMemorySessionStore, InMemoryMemoryStore | default stores |
| buildSystemPrompt, renderMemoryIndex | prompt assembly helpers |
| @agentaily/agent-loop/providers → openaiCompatible, deepseek | LLM adapters |
| @agentaily/agent-loop/adapters/cf-kv → KVSessionStore, KVMemoryStore | Cloudflare KV storage |
AgentOptions
| option | default | notes |
| --- | --- | --- |
| provider | — | required; an LLMProvider |
| instructions | — | base system prompt (persona / rules) |
| tools | [] | always-available app tools |
| skills | [] | Skill[] or a SkillRegistry |
| memory | new InMemoryMemoryStore | long-term facts |
| sessions | new InMemorySessionStore | conversation history |
| builtins | true | inject load_skill / remember / recall |
| maxSteps | 8 | provider round-trips before bailing |
| temperature, maxTokens | — | forwarded to the provider |
| onStep | — | (event) => void per loop step |
Bring your own provider
Implement one method:
import type { LLMProvider } from '@agentaily/agent-loop'
const myProvider: LLMProvider = {
async chat({ system, messages, tools, temperature, maxTokens, signal }) {
// call your model, return { content, toolCalls? }
return { content: '...', toolCalls: [] }
},
}Develop
npm install
npm test # vitest (mocked provider — no network)
npm run typecheck
npm run build # tsup -> dist (ESM + d.ts)License
MIT © agentaily
