Build AI tools first. Compose agents when you need them.
Quick Start · Mental Model · Choose a Primitive · Capability Ladder · Providers · Examples · Docs
العربية · বাংলা · Deutsch · Español · Français · हिन्दी · Indonesia · 日本語 · 한국어 · Nederlands · ਪੰਜਾਬੀ · Polski · Português · Русский · Svenska · తెలుగు · ไทย · Türkçe · Українська · 粵語 · 简体中文 · 繁體中文
openFunctions is an MIT-licensed TypeScript framework for building AI-callable tools and exposing them through MCP, chat adapters, workflows, and agents. Its core runtime is simple:
ToolDefinition -> ToolRegistry -> AIAdapter
Everything else composes on top of that:
workflowsare deterministic orchestration around toolsagentsare LLM loops over a filtered registrystructured outputis a synthetic tool patternmemoryandragare stateful systems that can be wrapped back into tools
If you understand the tool runtime, the rest of the framework stays legible.
defineTool() -> registry.register() -> adapter/server executes tool
-> workflows compose tools
-> agents use filtered tools
-> memory/rag expose more tools
git clone https://github.com/Tom-R-Main/openFunctions.git
cd openFunctions
bash setup.sh
cp .env.example .env
npm run test-toolsThe first thing to build is a tool, not an agent.
A tool is your business logic plus a schema the AI can read:
import { defineTool, ok } from "../framework/index.js";
export const rollDice = defineTool({
name: "roll_dice",
description: "Roll a dice with the given number of sides",
inputSchema: {
type: "object",
properties: {
sides: { type: "number", description: "Number of sides (default 6)" },
},
},
handler: async ({ sides }) => {
const rolled = Math.floor(Math.random() * ((sides as number) || 6)) + 1;
return ok({ rolled });
},
});That one definition can be:
- executed directly by
registry.execute() - exposed to Claude/Desktop over MCP
- used inside the interactive chat loop
- composed into workflows
- filtered into agent-specific registries
Read more: Architecture
| Use this | When you want | What it really is |
|---|---|---|
defineTool() |
callable AI-facing business logic | the core primitive |
createChatAgent() |
a composable, embeddable AI agent | tools + memory + context + adapter in one config |
pipe() |
deterministic orchestration | code-driven tool/LLM pipeline |
defineAgent() |
adaptive multi-step tool use | an LLM loop over a filtered registry |
createConversationMemory() / createFactMemory() |
thread/fact state | persistence plus memory tools |
createRAG() |
semantic document retrieval | pgvector + embeddings + tools |
connectProvider() |
external system context | structured tools from Siftable, Obsidian, etc. |
createStore() / createPgStore() |
persistence | storage layer, not retrieval |
Rule of thumb:
- Start with a tool.
- Use
createChatAgent()when you want a complete agent with memory and context. - Use a workflow when you know the sequence.
- Use
defineAgent()when you need specialized agents inside crews. - Add memory for state you control.
- Add RAG for document retrieval by meaning.
- Add a context provider when you need external systems (tasks, calendars, CRM).
npm run create-tool expense_trackerEdit src/my-tools/expense_tracker.ts, then run:
npm run test-tools
npm testnpm start
npm run chat -- geminiThe same registry powers both.
Workflows are the default “advanced” primitive because the control flow stays explicit:
import { pipe, toolStep, llmStep } from "./framework/index.js";
const research = pipe(toolStep(registry, "define_word"))
.then(async (result) => result.data?.meanings?.[0] ?? "")
.then(llmStep(adapter, registry, "Explain this simply: {{input}}"));
await research.run({ word: "ephemeral" });createChatAgent() composes tools, memory, context providers, and an AI adapter into a single embeddable agent:
import { createChatAgent } from "./framework/index.js";
const agent = await createChatAgent({
provider: "gemini",
preset: "study-buddy",
memory: true, // conversation + fact memory (on by default)
providers: ["siftable"], // connect external context
});
// Use it four ways:
await agent.interactive(); // CLI
const result = await agent.chat("Create a task"); // programmatic
for await (const chunk of agent.chat("hello", { stream: true })) { ... } // streaming
await agent.serve({ port: 3000 }); // HTTP serverThe same config works from code, CLI flags, or YAML files. Memory is on by default — the agent remembers across sessions.
defineAgent() is for specialized agents inside crews and workflows — filtered registries and reasoning loops:
import { defineAgent } from "./framework/index.js";
const researcher = defineAgent({
name: "researcher",
role: "Research Analyst",
goal: "Find accurate information using available tools",
toolTags: ["search"],
});Use crews when multiple specialized agents need to collaborate.
Persistence:
const tasks = createStore<Task>("tasks");
const tasksPg = await createPgStore<Task>("tasks");Memory:
const conversations = createConversationMemory();
const facts = createFactMemory();
registry.registerAll(createMemoryTools(conversations, facts));RAG:
const rag = await createRAG({ embeddingProvider: "gemini" });
registry.registerAll(rag.createTools());RAG docs: docs/RAG.md
Context providers bring external systems (task managers, calendars, CRM, knowledge bases) into the agent runtime as tools:
import { connectProvider, contextPrompt } from "./framework/index.js";
import { createSiftableProvider } from "./providers/execufunction/index.js";
// Connect — registers tools tagged "context" + "context:siftable"
const sift = await connectProvider(
createSiftableProvider({ token: process.env.SIFT_TOKEN }),
registry,
);
// Inject active tasks + upcoming events into agent system prompts
const context = await contextPrompt([sift]);The ContextProvider interface is pluggable — implement metadata, connect(), and createTools() to bring any backend into the framework. See Architecture for the full interface.
| Provider | Status | Capabilities |
|---|---|---|
| Siftable | Built-in | human planning, agent work, projects, knowledge, relationships, calendar, code, vault, governed AI and optional datasets |
| Obsidian | Template (planned) | knowledge |
| Notion | Template (planned) | knowledge, tasks, projects |
npm run test-tools # Interactive CLI — test tools locally
npm run dev # Dev mode — auto-restarts on save
npm test # Run tool-defined automated tests
npm run typecheck # Native TypeScript 7 diagnostics
npm run typecheck:ts6 # TypeScript 6 compatibility diagnostics
npm run verify:typescript # Verify TS7/TS6 ownership and emitted parity
npm run chat # Chat with AI using your tools
npm run chat -- gemini # Force a specific provider
npm run chat -- --no-memory # Chat without persistent memory
npm run create-tool <name> # Scaffold a new tool
npm run docs # Generate tool reference docs
npm run inspect # MCP Inspector web UI
npm start # Start MCP server for Claude Desktop / CursorSet one API key in .env and the chat loop will auto-detect the provider.
Defaults are resolved by workload role, then the exact choice is recorded in each run manifest. The catalog is dated so model churn stays out of application code. These are the current defaults for the adapters' default roles:
| Provider | Default role | Current model | API |
|---|---|---|---|
| Gemini | instant |
gemini-3.7-flash |
Function calling |
| OpenAI | expert |
gpt-5.6-terra (medium) |
Responses API |
| Anthropic | expert |
claude-sonnet-5 (medium) |
Messages + adaptive thinking + tool_use |
| xAI | expert |
grok-4.5 (medium) |
Responses API |
| OpenRouter | instant |
google/gemini-3.7-flash |
OpenAI-compatible |
Choose a stable role when you do not need to pin a model:
const agent = await createChatAgent({
provider: "openai",
modelRole: "frontier", // currently gpt-5.6-sol with low reasoning
});Examples:
npm run chat
npm run chat -- gemini
npm run chat -- openai gpt-5.6-terra
npm run chat -- gemini --prompt study-buddyTests live with tool definitions:
defineTool({
name: "create_task",
// ...
tests: [
{ name: "creates a task", input: { title: "Read ch5", subject: "Bio" }, expect: { success: true } },
{ name: "fails without subject", input: { title: "Read ch5" }, expect: { success: false } },
],
});The registry validates parameters before handlers run, so schema errors are surfaced clearly enough for both humans and LLMs to recover.
| Domain | Tools | Pattern |
|---|---|---|
| Study Tracker | create_task, list_tasks, complete_task |
CRUD + Store |
| Bookmark Manager | save_link, search_links, tag_link |
Arrays + Search |
| Recipe Keeper | save_recipe, search_recipes, get_random |
Nested Data + Random |
| Expense Splitter | add_expense, split_bill, get_balances |
Math + Calculations |
| Workout Logger | log_workout, get_stats, suggest_workout |
Date Filtering + Stats |
| Dictionary | define_word, find_synonyms |
External API (no key) |
| Quiz Generator | create_quiz, answer_question, get_score |
Stateful Game |
| AI Tools | summarize_text, generate_flashcards |
Tool Calls an LLM |
| Utilities | calculate, convert_units, format_date |
Stateless Helpers |
- Architecture: the runtime model, filtered registries, synthetic tools, and execution paths
- Legible runs: run manifests, outcome claims, verification, assurance, and fulfillment
- TypeScript 7 and 6: native TS7 builds with the TS6 compatibility/programmatic API
- Agent runtimes: Hermes MCP, Pi extensions, and current OpenClaw tool plugins
- Siftable integration: current MCP/CLI boundaries, auth, tool projection, and drift checks
- RAG: semantic chunking, Gemini/OpenAI embeddings, pgvector schema, HNSW search, and tool integration
src/providers/execufunction/ exposes Siftable
(formerly ExecuFunction) as an openFunctions ContextProvider.
The provider projects its default tool set directly from the published
@siftable/mcp-server
SDK. That keeps tool names, schemas, feature gates, transport containment,
work-item lifecycle rules, structured receipts, and execution behavior aligned
with the installed Siftable MCP version. The current 1.2.27 package declares
136 tools; 114 are callable with its default feature flags. Optional dataset
and ontology tools appear automatically when their Siftable feature flags are
enabled. Set includeLegacyAliases: true only when migrating callers that still
invoke the old hand-written exf_* names.
import { connectProvider, registry } from "openfunction/framework";
import { createSiftableProvider } from "openfunction/providers/execufunction";
const sift = await connectProvider(createSiftableProvider(), registry);Auth resolution: explicit { token } argument → SIFT_TOKEN (current CLI
convention) → SIFT_PAT (MCP convention) → legacy EXF_TOKEN / EXF_PAT.
sift auth login saves credentials for the CLI; embedded OpenFunction and
OpenClaw processes still need an explicit token or one of those environment
variables. The API URL and workspace ID retain SIFT_* then legacy EXF_*
fallbacks.
The ordinary sift CLI is the human/operator surface. In the current CLI,
sift tasks manages human planning, sift work manages the distinct executable
agent queue, sift capabilities --json reports readiness, and
sift doctor --json diagnoses auth/API/workspace configuration. The separate
interactive TUI is not treated as an MCP transport or as command parity work.
Run tsx scripts/test-siftable-live.ts (with SIFT_TOKEN or SIFT_PAT set) to
verify the provider actually round-trips against your account.
OpenFunction now has dependency-free adapters for three external agent hosts:
- Hermes Agent:
createHermesMcpConfig()produces a least-privilege MCP configuration fragment with an explicit registry-tool allowlist. - Pi (
@earendil-works/pi):registerPiTools()andtoPiTools()produce native Pi extension tools with correct throw-on-failure behavior. - OpenClaw:
toOpenclawToolPluginTools()targets the currentdefineToolPlugin()generated-contract path. The existingtoOpenclawTools()adapter remains for mixed or dynamic plugins.
See Agent runtime integrations for exact host setup and compatibility boundaries.
src/framework/openclaw.ts exports toOpenclawTools(registry), which
converts an openFunctions ToolRegistry into the compatibility shape
OpenClaw's api.registerTool() expects. The newer
toOpenclawToolPluginTools() adapter returns static definitions for
defineToolPlugin() and generated contracts.tools metadata. Neither bridge
adds an OpenClaw runtime dependency to the framework.
Two reference plugins live in plugins/:
openclaw-execufunction/— Siftable for OpenClaw. It derives all currently enabled tools from@siftable/mcp-server, delegates execution to the SDK, and generates its staticcontracts.toolsmanifest from the same source. Plugin idexecufunctionremains for config compatibility.openclaw-openfunctions/— Current tool-only reference usingdefineToolPlugin(), compiled ESM, and generated manifest metadata. Markedprivate:true; it imports the colocated framework by relative path.
Install the Siftable plugin in openclaw:
openclaw plugins install @openfunctions/openclaw-execufunctionSet SIFT_TOKEN or SIFT_PAT (legacy EXF_TOKEN / EXF_PAT also work) in
the environment, or use OpenClaw plugin settings.
openFunctions/
├── src/
│ ├── framework/ # Core runtime + composition layers
│ │ ├── chat-agent.ts # createChatAgent() — composable chat agent factory
│ │ ├── chat-agent-types.ts # ChatAgent, ChatAgentConfig, ChatResult types
│ │ ├── chat-agent-resolve.ts # Config resolution, provider auto-detection
│ │ ├── chat-agent-http.ts # HTTP server for agent.serve()
│ │ ├── context.ts # Context provider interface
│ │ └── ... # tool, registry, agents, memory, rag, workflows
│ ├── providers/
│ │ └── execufunction/ # Siftable context provider (wraps @siftable/mcp-server)
│ ├── examples/ # Reference tool patterns
│ ├── my-tools/ # Your tools
│ └── index.ts # MCP entrypoint
├── plugins/
│ ├── openclaw-execufunction/ # Siftable plugin for openclaw (publishable)
│ ├── openclaw-openfunctions/ # Reference: current tool-plugin bridge (private)
│ └── pi-openfunctions/ # Reference: native Pi extension bridge (private)
├── docs/ # Architecture docs
├── scripts/ # chat, create-tool, docs
├── test-client/ # CLI tester + test runner
├── system-prompts/ # Prompt presets
└── package.json
MIT — see LICENSE