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.