Ageniti
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 8 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
A standard SDK for building agent-facing apps across MCP, CLI, React, and AI tool stacks.
Ageniti
The action primitive for apps that need to be callable by agents.
Website · GitHub · npm · Getting Started · API
Ageniti is the action primitive layer for apps that need to expose
capabilities to agents, automation systems, and external tools. You define an
action once — input contract, output contract, side effects, permissions —
and any caller (CLI, HTTP, MCP, OpenAI / AI SDK tools, React UI, your own
typed client) can invoke it through the same runtime, with the same
streaming events, the same redaction, the same error contract.
Why Ageniti
Modern apps want to be callable not just from people but from agents, scripts,
and other apps. Today every entry point comes with its own glue: argv parsing,
schema validation, tool descriptions, permission checks, log redaction, error
shapes, idempotency, cancellation. Each surface re-implements the same things
inconsistently.
Ageniti collapses all of that into one concept. Each action you declare runs
through a single runtime that handles the cross-cutting concerns. Each surface
is a thin adapter over the same contract. Streaming events let any consumer —
agent caller, UI, log shipper — observe the action live without owning it.
Install
npm i @ageniti/core
Subpath exports:
| Subpath | What it gives you |
|---|---|
@ageniti/core |
Main authoring API plus packaging/project helpers such as buildArtifacts, packageArtifacts, publishArtifacts, createGuideDoc, exportDocs, initProject, doctorProject, and detectTypeScriptRuntime |
@ageniti/core/ai-sdk |
createOpenAITools, createOpenAIResponsesTools, createAISDKTools, createFunctionCallingManifest |
@ageniti/core/adapters |
Built-in surface adapters and helpers such as httpAdapter, mcpAdapter, aiSdkAdapter, cliAdapter, jsonAdapter, reactAdapter, devAdapter, defaultSurfaceAdapters, defineSurfaceAdapter, and findAdapter |
@ageniti/core/app, /core, /cli, /mcp, /dev, /manifest, /json-runner, /lint |
Narrow imports for the corresponding runtime or surface modules |
@ageniti/core/http |
createHttpHandler, createHttpServer, parseRequestBody, sendJson, sendText |
@ageniti/core/handlers |
defineActions, actionFromHandler, actionsFromHandlers |
@ageniti/core/schema-adapter |
wrapSchema, Zod / Standard Schema v1 interop |
@ageniti/core/schema |
Schema helpers only |
@ageniti/core/client |
createClient, AgenitiClientError |
@ageniti/core/client-gen |
generateClientTypes, jsonSchemaToTs |
@ageniti/core/test-utils |
createTestRuntime, expectOk, expectError, expectLog, collectStream, stubAction |
@ageniti/core/react |
createReactActionAdapter, makeInvoker, streamAction (no React import) |
@ageniti/core/react-hooks |
useAction — full state-machine hook (React peer dep) |
@ageniti/core/package.json |
Package metadata for tooling that needs to inspect the published package |
The 5 Core Primitives
1. The Action Contract
import { defineAction, s } from "@ageniti/core";
export const createTask = defineAction({
name: "create_task",
description: "Create a task in the user's inbox.",
sideEffects: "write",
idempotency: "conditional",
input: s.object({
title: s.string().min(1),
priority: s.enum(["low", "high"]).default("low"),
}),
output: s.object({ id: s.string(), title: s.string() }),
async run({ title, priority }, ctx) {
ctx.logger.info("Creating task", { title });
const task = await ctx.services.tasks.create({ title, priority });
return { id: task.id, title: task.title };
},
});
The contract is the source of truth. Every surface — CLI flags, MCP tool
definition, OpenAI tool spec, HTTP route, React hook, typed client — is
derived from it. You never describe the same action twice.
2. Bring Your Own Schema
You don't have to use s.*. Zod, Valibot, ArkType, anything that quacks
like Standard Schema v1 just works:
import { z } from "zod";
import { defineAction } from "@ageniti/core";
export const search = defineAction({
name: "search_tasks",
description: "Search for tasks matching a query.",
input: z.object({ query: z.string(), limit: z.number().int().optional() }),
output: z.object({ results: z.array(z.object({ id: z.string(), title: z.string() })) }),
async run({ query, limit }) {
return { results: await tasks.search(query, limit ?? 20) };
},
});
Ageniti detects foreign schemas (anything with .safeParse / .parse /"~standard".validate) and wraps them transparently. JSON Schema for MCP
and OpenAI tool descriptions is generated from the wrapped schema.
3. Bulk-Wrap Functions You Already Have
For Next.js Server Actions, tRPC procedures, or any plain functions:
import { actionsFromHandlers, s } from "@ageniti/core";
import * as handlers from "./app/actions/tasks"; // your existing functions
export const actions = actionsFromHandlers(handlers, {
createTask: {
description: "Create a task.",
input: s.object({ title: s.string() }),
sideEffects: "write",
},
searchTasks: {
description: "Search tasks.",
input: s.object({ query: s.string() }),
},
});
Or defineActions for full control:
import { defineActions, s } from "@ageniti/core";
export const actions = defineActions({
createTask: {
description: "Create a task.",
input: s.object({ title: s.string() }),
run: async ({ title }) => tasks.create({ title }),
},
// function shorthand for read-only no-input actions
ping: () => ({ ok: true, time: Date.now() }),
});
CamelCase keys are normalized to snake_case action names automatically.
4. Streaming Events
Every action runs through a runtime that emits live events as it
executes. UIs, agents, and log shippers can subscribe without owning the
action:
const events = runtime.stream("create_task", { title: "Ship v1" });
for await (const event of events) {
if (event.type === "log") console.log(event.level, event.message);
if (event.type === "progress") updateProgressBar(event.percent);
if (event.type === "artifact") attachToUi(event.artifact);
if (event.type === "result") finalize(event.envelope);
}
Events come from ctx.logger.*, ctx.progress.report(), andctx.artifacts.add() inside your run() function. The CLI's --ndjson
mode and the React hook are both built on this primitive.
5. Typed Client + Codegen
import { createClient } from "@ageniti/core/client";
// In-process
const client = createClient({ runtime });
const task = await client.create_task({ title: "Hello" });
// ^? { id: string; title: string }
// Or talk to a remote @ageniti HTTP server
const remote = createClient({ url: "https://api.example.com" });
const tasks = await remote.search_tasks({ query: "today" });
Remote HTTP clients can send metadata, confirm, and idempotencyKey.
Trusted user / auth must be resolved server-side via headers orresolveContext, not passed in the request body.
Generate .d.ts for the typed client surface from your action manifest:
import { generateClientTypes } from "@ageniti/core/client-gen";
import { writeFile } from "node:fs/promises";
await writeFile(".ageniti/client.d.ts", generateClientTypes(actions));
React
Two layers: a stateless adapter and a state-machine hook.
// app/components/CreateTaskButton.tsx
"use client";
import { useAction } from "@ageniti/core/react-hooks";
import { runtime } from "@/src/ageniti/app";
import { createTask } from "@/src/ageniti/actions/tasks";
export function CreateTaskButton() {
const { invoke, status, data, error, logs, progress, cancel } =
useAction(createTask, { runtime });
return (
<>
<button onClick={() => invoke({ title: "Hello" })} disabled={status === "loading"}>
{status === "loading" ? `${progress?.percent ?? 0}%` : "Create"}
</button>
{status === "loading" && <button onClick={cancel}>Cancel</button>}
{status === "success" && <p>Created task {data.id}</p>}
{status === "error" && <p>Error: {error.message}</p>}
<pre>{logs.map((l) => l.message).join("\n")}</pre>
</>
);
}
The hook subscribes to runtime.stream so logs / artifacts / progress
update live during the invocation. Unmounts auto-abort.
Exposing Surfaces
import { createAgenitiApp, createMcpStdioServer } from "@ageniti/core";
import { actions } from "./actions";
export const app = createAgenitiApp({
name: "tasks",
actions,
description: "Task management actions for operators, automation, and agent callers.",
});
// CLI
app.createCli().main();
// MCP stdio (auto-detects Content-Length or newline framing)
createMcpStdioServer({ actions: app.actions, runtime: app.runtime }).start();
// HTTP (Express / Hono / Next.js Route Handler / raw Node)
const handler = app.createHttpHandler();
// OpenAI / AI SDK tool specs
const openai = app.createOpenAITools();
const responses = app.createOpenAIResponsesTools();
const aiSdk = app.createAISDKTools();
const manifest = app.createFunctionCallingManifest();
Each surface is generated from the same action contract.
Runtime Capabilities
The runtime handles all the cross-cutting concerns so your run() function
stays focused on business logic:
- Validation (input, output, JSON-serializable check)
- Permission gating (overridable
permissionChecker) - Confirmation gate for destructive actions (machine surfaces require
{ confirm: true }or surface to bereact/dev) - Idempotency —
idempotencyKeyreplays a cached envelope scoped by
action, validated input, surface, and trusted caller fingerprint; LRU cap
keeps memory bounded - Concurrency limits per action — returns
CONCURRENCY_LIMIT(retryable) - Timeout + retry — per-attempt
AbortControllerso retries see fresh
signal - Cancellation — external
signal, CLI SIGINT, React unmount all wired - Streaming events — log / progress / artifact / result
- Redaction — log fields, artifact metadata, error messages (Bearer /
JWT /sk-style tokens) - Hooks —
onInvocationStart,onInvocationEndfor telemetry - Deprecated warnings — emitted on every invocation of a deprecated action
The Envelope
Every invocation returns the same envelope shape:
{
ok: true,
data: { /* validated output */ },
artifacts: [...],
logs: [...],
meta: { action, invocationId, surface, durationMs, idempotent? },
}
Or on failure:
{
ok: false,
error: { code, message, issues, retryable },
artifacts: [...],
logs: [...],
meta: { ... },
}
Standard error codes are exported as ERROR_CODES:
ACTION_NOT_FOUND, VALIDATION_ERROR, OUTPUT_VALIDATION_ERROR,
OUTPUT_SERIALIZATION_ERROR, AUTHENTICATION_ERROR, AUTHORIZATION_ERROR,
RATE_LIMITED, TIMEOUT, CANCELLED, CONFLICT, EXTERNAL_SERVICE_ERROR,
INTERNAL_ERROR, UNSUPPORTED_SURFACE, UNSAFE_ACTION,
CONFIRMATION_REQUIRED, CONCURRENCY_LIMIT
HTTP maps these to 400 / 401 / 403 / 404 / 405 / 409 / 413 / 415 / 429 / 499 / 500 / 502 / 504. CLI maps them to 0 / 1 / 2 / 3 / 4 / 5 / 124 / 130.
Testing
import { createTestRuntime, expectOk, expectError, collectStream } from "@ageniti/core/test-utils";
import { createTask } from "./actions/tasks";
test("create_task happy path", async () => {
const t = createTestRuntime([createTask], { services: { tasks: stubTasksService } });
const env = await t.invoke("create_task", { title: "Hello" });
const data = expectOk(env);
expect(data.title).toBe("Hello");
});
test("rejects empty title", async () => {
const t = createTestRuntime([createTask]);
const env = await t.invoke("create_task", { title: "" });
expectError(env, "VALIDATION_ERROR");
});
test("emits progress events", async () => {
const t = createTestRuntime([longRunning]);
const events = await collectStream(t.stream("long_running", {}));
const progress = events.filter((e) => e.type === "progress");
expect(progress.length).toBeGreaterThan(0);
});
Drop-In Into An Existing App
You don't restructure your app. Pick the functions you want to expose,
declare them as actions, mount the surfaces:
// app/actions/tasks.ts — your existing Server Actions / handlers
"use server";
export async function createTask(input: { title: string }) { /* ... */ }
// src/ageniti/app.ts — new
import { createAgenitiApp, actionsFromHandlers, s } from "@ageniti/core";
import * as handlers from "@/app/actions/tasks";
export const app = createAgenitiApp({
name: "tasks",
actions: actionsFromHandlers(handlers, {
createTask: {
description: "Create a task.",
input: s.object({ title: s.string() }),
sideEffects: "write",
},
}),
});
// app/api/[[...ageniti]]/route.ts
import { app } from "@/src/ageniti/app";
const handler = app.createHttpHandler();
export { handler as GET, handler as POST };
That's it. The same actions are now reachable from CLI (npx ageniti create-task --title hello), MCP (ageniti mcp --stdio), HTTP (/ageniti/...),
OpenAI / AI SDK tools, and the React hook.
Examples
| File | Shows |
|---|---|
| examples/hello.cli.js | Minimum viable action + CLI |
| examples/task-app.js | Real-world app with multiple actions |
| examples/demo.cli.js | Multi-surface demo app with CLI, dev, and MCP modes |
| examples/buildable-app.mjs | Minimal build-safe app export for launcher/package flows |
| examples/streaming.js | Live progress / log streaming |
| examples/zod-action.js | Using Zod-style schemas |
| examples/typed-client.js | Typed in-process client, raw envelopes, streams, and client codegen |
| examples/bulk-handlers.js | defineActions / actionsFromHandlers |
| examples/test-helpers.test.js | Testing actions |
| examples/openai-responses-host.js | OpenAI Responses tool spec |
| examples/ai-sdk-route.js | Vercel AI SDK tool integration |
| examples/http-gateway.js | HTTP server with detailed status codes |
| examples/mcp-host.js | MCP host calling actions |
Documentation
Contributing
PRs and issues welcome. See CONTRIBUTING.md.
Security
See SECURITY.md for vulnerability reporting.
License
MIT — see LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found