Agentic Primitives
This page walks through the five core primitives Ageniti gives you, in the order you usually encounter them.
1. The Action Contract
Every callable capability is an action: a typed input/output pair plus a declarative side-effects policy. The contract is the source of truth for every surface.
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 };
},
});Required fields are name, description, and run. Reserved names that
clash with built-in CLI commands (actions, mcp, dev, …) are rejected
at definition time.
2. Bring Your Own Schema
defineAction accepts any schema implementing Standard Schema v1 or any
Zod-like schema (.safeParse, .parse):
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) };
},
});Foreign schemas are detected by duck typing. JSON Schema for MCP tool
definitions and OpenAI tool specs is generated through best-effort
introspection (ZodObject, ZodArray, ZodUnion, ZodOptional,
ZodDefault, etc.).
If your schema is exotic, override the JSON Schema:
import { wrapSchema } from "@ageniti/core/schema-adapter";
const wrapped = wrapSchema(myCustomSchema, {
jsonSchema: { type: "object", properties: { … } },
});3. Bulk-Wrap Existing Functions
For Next.js Server Actions, tRPC procedures, plain handler records:
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 for full control, defineActions accepts a record of configs (or plain
functions for read-only no-input cases):
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 }),
},
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. Any consumer (UI, agent, log shipper) can subscribe via
runtime.stream():
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(), and
ctx.artifacts.add() inside your run() function. The CLI's --ndjson
mode and the React useAction hook are both built on this primitive.
5. Typed Client + Codegen
A typed client wraps any runtime or remote @ageniti HTTP server. Calls
return validated data on success and throw AgenitiClientError on
failure. Remote HTTP mode can send metadata, confirm, and
idempotencyKey, but trusted user / auth must be resolved server-side:
import { createClient } from "@ageniti/core/client";
// In-process
const client = createClient({ runtime });
const task = await client.create_task({ title: "Hello" });
// Or talk to a remote @ageniti HTTP server
const remote = createClient({ url: "https://api.example.com" });
const tasks = await remote.search_tasks({ query: "today" });Generate .d.ts for IDE autocomplete on the consumer side:
import { generateClientTypes } from "@ageniti/core/client-gen";
import { writeFile } from "node:fs/promises";
await writeFile(".ageniti/client.d.ts", generateClientTypes(actions, {
interfaceName: "TasksClient",
}));React Hook (built on the streaming primitive)
For React UI, the useAction hook subscribes to runtime.stream so logs,
artifacts, and progress update live during the invocation:
import { useAction } from "@ageniti/core/react-hooks";
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>}
</>
);
}State machine: idle → loading → (success | error | cancelled). Component
unmount auto-aborts. New invoke calls cancel the previous one.
Test Utilities
import {
createTestRuntime, expectOk, expectError, collectStream,
} from "@ageniti/core/test-utils";
test("create_task happy path", async () => {
const t = createTestRuntime([createTask], { services: { tasks: stubTasks } });
const env = await t.invoke("create_task", { title: "Hello" });
const data = expectOk(env);
expect(data.title).toBe("Hello");
});Framework-agnostic — works with node:test, vitest, jest. The
createTestRuntime helper preconfigures surface: "json" and bypasses the
confirmation gate so destructive actions can be tested without ceremony.
How It Fits Together
The five primitives above are the foundation. Every other surface — CLI generation, MCP server, OpenAI tool specs, AI SDK tools, HTTP server — is a thin adapter over the same action contract and the same streaming runtime. Whatever you build on top (agents, planners, workflows, custom UIs) reads from the same contract and subscribes to the same events.