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 -> ToolExecutor -> adapter or host
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.
The package root is a library. Importing it does not load .env, register
workshop tools, create stores, or start a server. Build and pack this checkout
to try the revised package before publication:
npm run build
npm pack
# In a separate consumer:
npm install /absolute/path/to/openfunction-1.0.0.tgzimport { defineTool, ToolRegistry, ok } from "openfunction/framework/core";
const registry = new ToolRegistry();
registry.register(defineTool({
name: "hello_world",
description: "Return a greeting for the supplied name",
inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
handler: async ({ name }) => ok({ greeting: `Hello ${name}` }),
}));
export { registry };Serve an exported registry with openfunction serve --module /absolute/path/to/tools.js. The existing npm start, npm run dev, and
workshop commands still use the source workshop entry. Siftable and PostgreSQL
are optional peers; root, core, and framework imports work without either.
The opt-in work profile selects a finite trusted step catalog, caller-owned state and grants, an explicit local work item, and exact verification bindings. The compiled CLI reviews and issues authority separately from Hermes's tool approval gate:
openfunction work prepare --config /absolute/profile.json --work-item job --out /absolute/preview.json
openfunction work authorize --config /absolute/profile.json --preview /absolute/preview.json
openfunction hermes-config --config /absolute/profile.json
openfunction serve --profile work --config /absolute/profile.jsonAuthorization displays the bound scope and limits. Type AUTHORIZE to permit
execution; automatic completion stays off unless you separately type
COMPLETE. Changes to the configuration, contract, or plan invalidate the
preview. Grant revocation takes effect at the next admission and completion.
See runtime integrations for profile configuration,
portable installation, and the verified host commands.
The controller records effects and independent verification, renews a bounded lease during long steps, and refuses completion with stale proof or unknown effects. It covers actions performed through its executor. Native host actions remain outside that journal. Local grant files assume trusted same-user code; they provide no sandbox against a process that can modify their custody. Hosted Siftable calls use authenticated MCP, but its durable-work backend is disabled until the missing public recovery and parser contracts are available.
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 development SDK is pinned to
1.5.1; drift checks compare its actual enabled catalog and schemas. Hosted
session tools require custody unavailable to this embedded provider and are
excluded. Optional dataset and ontology tools follow their Siftable feature
flags. 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/— Mixed Siftable and opt-in bounded-work plugin. It projects the supported embedded SDK catalog through the shared executor, resolves credentials only on invocation, and snapshots exact tool contracts. Plugin idexecufunctionremains for config compatibility. Its legacy mixed entry uses the SDK registration path; the static validator targets the separate tool-only plugin below.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
│ ├── runtime/ # Trusted bounded-work profile loader
│ ├── cli.ts # Compiled serve and grant commands
│ ├── workshop.ts # Source workshop MCP entrypoint
│ └── index.ts # Side-effect-free library 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)
│ └── hermes-openfunctions/ # Portable MCP plugin and bounded-work skill
├── docs/ # Architecture docs
├── scripts/ # chat, create-tool, docs
├── test-client/ # CLI tester + test runner
├── system-prompts/ # Prompt presets
└── package.json
MIT — see LICENSE