@tinyweb_dev/agent-framework-agent
v0.4.0
Published
Public entry point for building agents. Combines the `llm`, `tool`, `protocol`, and `core` packages into a friendly Promise/AsyncIterator API. **Effect-TS is hidden** from the public surface.
Readme
@tinyweb-agent-framework/agent
Public entry point for building agents. Combines the llm, tool, protocol, and core packages into a friendly Promise/AsyncIterator API. Effect-TS is hidden from the public surface.
Install
bun add @tinyweb-agent-framework/agent @tinyweb-agent-framework/llmdefineAgent(config)
import { defineAgent } from "@tinyweb-agent-framework/agent"
import { OpenAI } from "@tinyweb-agent-framework/llm"
const agent = defineAgent({
name: "my-agent",
model: OpenAI.chat("gpt-4o-mini", { apiKey: process.env.OPENAI_API_KEY! }),
systemPrompt: "Helpful, concise.",
// tools, permission, plugins, mcp, skillDirs, store, stopWhen, generation … all optional
})Streaming
for await (const event of agent.run({ prompt: "Hello", sessionId: "abc" })) {
if (event.type === "text-delta") process.stdout.write(event.text)
if (event.type === "tool-call") console.log("[tool]", event.name, event.input)
}One-shot
const { text, usage } = await agent.runOnce({ prompt: "Summarise Bun in one line" })Express / HTTP
const handler = agent.toSSEHandler({ format: "ai-sdk" }) // or "tinyweb"
app.post("/agent", (req, res) => handler({ prompt: req.body.prompt }, res))defineTool(config)
import { defineTool } from "@tinyweb-agent-framework/agent"
import { Schema } from "effect"
const slugify = defineTool({
description: "Slugify a string",
parameters: Schema.Struct({ text: Schema.String }),
execute: async ({ text }) =>
({ slug: text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") }),
})Effect-style execute is also accepted (return Effect<S, ToolFailure>).
Sessions, history & storage
Pass sessionId to agent.run({...}) or agent.runOnce({...}) and the agent automatically:
- Persists every user prompt and assistant reply to the configured store.
- Replays the full conversation into the LLM on the next turn.
for await (const event of agent.run({ sessionId: "user-42-thread-1", prompt: "Hello" })) {
if (event.type === "text-delta") process.stdout.write(event.text)
}
// Later — model still remembers
await agent.runOnce({ sessionId: "user-42-thread-1", prompt: "What did I say?" })Storage adapters
// In-memory (default — process-lifetime)
import { makeMemoryStore } from "@tinyweb-agent-framework/agent"
// SQLite via bun:sqlite (single file, zero config)
import { makeSqliteStore } from "@tinyweb-agent-framework/agent/session/sqlite"
// Postgres via drizzle-orm/postgres-js (peer dep: postgres)
import { makePostgresStore } from "@tinyweb-agent-framework/agent/session/postgres"
const agent = defineAgent({
...,
store: makeSqliteStore("./agent.db"),
})The SQLite/Postgres adapters live at subpaths so bundlers that don't understand bun:sqlite (Next.js webpack, etc.) won't try to resolve them for stateless apps.
Session SDK — agent.sessions.*
Every agent exposes a Promise-based SDK to manage sessions and history. No Effect, no boilerplate:
const session = await agent.sessions.create({
title: "My chat",
metadata: { lang: "vi" },
ownerId: "user-42", // denormalised to an indexed column for fast list-by-owner
})
const { items, nextCursor } = await agent.sessions.list({
limit: 50,
ownerId: "user-42",
})
const messages = await agent.sessions.messages(session.id, { limit: 100 })
await agent.sessions.setTitle(session.id, "Renamed")
await agent.sessions.deleteMessage(session.id, lastMsgId)
await agent.sessions.delete(session.id)Full surface:
agent.sessions.create({ title?, metadata?, ownerId? }) → Session.Info
agent.sessions.list({ limit?, cursor?, ownerId? }) → { items, nextCursor? }
agent.sessions.get(sessionId) → Session.Info | undefined
agent.sessions.delete(sessionId) → void
agent.sessions.setTitle(sessionId, title) → void
agent.sessions.setMetadata(sessionId, metadata) → void
agent.sessions.patch(sessionId, { title?, metadata? }) → Session.Info
agent.sessions.messages(sessionId, { limit?, cursor? }) → { items, nextCursor? }
agent.sessions.deleteMessage(sessionId, messageId) → voidlist results are sorted newest-first by updatedAt and use cursor-based pagination (base64url-encoded — opaque to callers; just round-trip nextCursor into the next request). messages returns oldest-first.
REST endpoints — agent.toHTTPHandlers()
agent.toHTTPHandlers() returns ready-to-mount handlers (req, res) => Promise<void> compatible with Express, Hono, Fastify, native node:http, and Bun.serve (any framework that exposes req.method/req.url/req.params/req.body/res.setHeader/res.write/res.end).
import express from "express"
const app = express()
app.use(express.json())
const api = agent.toHTTPHandlers({
defaultFormat: "ai-sdk", // SSE format for /chat
authorize: async (req) => ({ // optional: derive ownerId from auth
ownerId: req.headers["x-user-id"] as string,
}),
})
const router = express.Router({ mergeParams: true })
router.get("/", api.listSessions)
router.post("/", api.createSession)
router.get("/:id", api.getSession)
router.patch("/:id", api.updateSession)
router.delete("/:id", api.deleteSession)
router.get("/:id/messages", api.listMessages)
router.delete("/:id/messages/:mid", api.deleteMessage)
router.post("/:id/chat", api.chat)
app.use("/sessions", router)
app.listen(3000)REST surface:
| Method | Path | Returns |
|----------|----------------------------------------|------------------------------------------|
| GET | /sessions?limit&cursor&ownerId | { items, nextCursor? } |
| POST | /sessions {title?, metadata?, ownerId?} | Session.Info |
| GET | /sessions/:id | Session.Info |
| PATCH | /sessions/:id {title?, metadata?} | Session.Info |
| DELETE | /sessions/:id | 204 |
| GET | /sessions/:id/messages?limit&cursor | { items, nextCursor? } (oldest-first) |
| DELETE | /sessions/:id/messages/:mid | 204 |
| POST | /sessions/:id/chat {prompt} or {messages} | text/event-stream (SSE) |
The chat handler streams in defaultFormat ("tinyweb" JSON or "ai-sdk" Vercel AI SDK Data Stream). Override per request with ?format=ai-sdk.
The authorize hook runs before every handler; return { ownerId } to enforce session ownership (ownerId is matched against metadata.ownerId on each session). Throw or return {} to fall back to no-auth.
Next.js App Router
Skip toHTTPHandlers() and call agent.sessions.* directly — Next.js routes already have their own Request/Response model:
// app/api/sessions/[id]/messages/route.ts
import { agent } from "@/lib/agent"
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const result = await agent.sessions.messages(id, { limit: 100 })
return Response.json(result)
}See examples/next-chat-ui/ for the full app.
Permission
const agent = defineAgent({
// ...
tools: { web_fetch, shell_exec },
permission: { web_fetch: "allow", shell_exec: "ask" }, // or just "allow" | "ask" | "deny"
askHandler: async ({ toolName, input }) => {
// confirm with the user via UI / Slack / etc.
return true
},
})MCP servers
const agent = defineAgent({
// ...
mcp: {
fs: { type: "stdio", command: "npx", args: ["@modelcontextprotocol/server-filesystem", "/tmp"] },
api: { type: "http", url: "https://example.com/mcp" },
},
})
// Tools auto-namespaced as fs__read_file, api__do_thing, ...Skills
Drop SKILL.md files in:
./skills/**/SKILL.md~/.tinyweb-agent/skills/**/SKILL.md- Any extra dirs you pass via
skillDirs: [...]
Each must include YAML frontmatter:
---
name: vietnamese-blog
description: Write Vietnamese blog posts in the company tone of voice
---
Detailed instructions go here…The agent gains a skill tool — the LLM calls it with { name } and gets the markdown body back.
Plugins / Hooks
const myPlugin = async ({ agentName }) => ({
async "chat.params"(_, out) {
out.temperature = 0.2
},
async "tool.execute.before"({ tool }, out) {
console.log("[before]", tool, out.args)
},
})
const agent = defineAgent({ ..., plugins: [myPlugin] })Supported hooks: event, chat.message, chat.params, chat.headers, tool.execute.before, tool.execute.after, tool.definition, permission.ask, experimental.chat.system.transform.
Bus
In-process pub/sub via PubSub.unbounded:
import { makeBus } from "@tinyweb-agent-framework/agent"
const bus = makeBus()
// publish / subscribe<K> / subscribeAllLicense
MIT
