Design Philosophy
Why Anvia favors stable TypeScript objects, explicit boundaries, and application-owned infrastructure.
Anvia is designed as an application-owned AI runtime for TypeScript teams, not a full application platform.
The core idea is simple: AI features should be ordinary TypeScript objects that your application creates, configures, tests, and passes around. Anvia owns the AI runtime boundary. Your application owns product behavior.
That boundary is deliberate:
- Anvia owns model adapters, agent execution, tool calling, schemas, streaming, pipelines, retrieval primitives, observers, and local Studio iteration.
- Your application owns users, auth, databases, memory, permissions, approvals, queues, audit logs, deployment, and side effects.
Stable Runtime Objects
Builders configure behavior. Built agents, extractors, tools, models, and pipelines are stable values that can be passed into routes, jobs, tests, Studio, or other workflows.
This makes behavior easier to reason about. An agent does not depend on a hidden global registry or framework runtime to discover its model, tools, context, or observers.
// app/ai.ts
import { AgentBuilder } from "@anvia/core";
import { OpenAIClient } from "@anvia/openai";
const client = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY });
const model = client.completionModel("gpt-5.5");
export const supportAgent = new AgentBuilder("support", model)
.instructions("Answer support questions clearly.")
.defaultMaxTurns(3)
.build();// app/routes/support.ts
import { supportAgent } from "../ai";
export async function POST(request: Request) {
const { message } = await request.json();
const response = await supportAgent.prompt(message).send();
return Response.json({ output: response.output });
}Advantage: the route receives an already-built object. Tests can import the same shape or replace it with a fake agent. There is no hidden app container that must be booted before the route can run.
Application-Owned Infrastructure
Anvia does not try to be the source of truth for your database, auth system, permission policy, queue, deployment topology, memory layer, or approval operations.
Those decisions usually affect product correctness and security, so they should stay in application code. Anvia gives you hooks, tool approval metadata, Studio approval UI for local iteration, observers, and storage interfaces where integration is useful, but the ownership boundary remains explicit.
import { createTool } from "@anvia/core";
import { z } from "zod";
import { requirePermission } from "./auth";
import { billing } from "./billing";
export function createRefundTool(userId: string) {
return createTool({
name: "issue_refund",
description: "Issue a refund for a paid order.",
input: z.object({
orderId: z.string(),
amount: z.number().positive(),
}),
async execute({ orderId, amount }) {
await requirePermission(userId, "refund:create");
return billing.refund({ orderId, amount });
},
});
}Advantage: the side effect remains ordinary product code. Your auth, audit logs, database transactions, and retries stay where your team already manages them.
Approval follows the same boundary. Core hooks can run, skip, or cancel a tool call, and Studio can interpret tool approval metadata while you are testing the agent. Your product still decides who is allowed to approve, where decisions are stored, how notifications work, and what audit trail is required.
import { createHook } from "@anvia/core";
const permissionHook = createHook({
async onToolCall({ toolName, tool }) {
if (toolName === "read_payroll") {
return tool.skip("Payroll data is restricted.");
}
if (toolName === "delete_account") {
const approved = await approvals.waitForDecision({
toolName,
reason: "Account deletion requires human approval.",
});
return approved ? tool.run() : tool.cancel("Account deletion was not approved.");
}
return tool.run();
},
});Advantage: Anvia owns the execution control point, but your application owns the permission decision and approval runtime.
Provider-Agnostic Model Boundary
Provider clients create reusable completion and embedding models. Agents and extractors depend on normalized model capabilities, not on one provider's runtime model.
This lets you use OpenAI, Anthropic, Gemini, Mistral, OpenAI-compatible APIs, or local embeddings without changing the way your application composes agents and workflows.
import { AgentBuilder } from "@anvia/core";
import { AnthropicClient } from "@anvia/anthropic";
import { OpenAIClient } from "@anvia/openai";
const openai = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY });
const anthropic = new AnthropicClient({ apiKey: process.env.ANTHROPIC_API_KEY });
const model =
process.env.PROVIDER === "anthropic"
? anthropic.completionModel("claude-opus-4-6")
: openai.completionModel("gpt-5.5");
export const agent = new AgentBuilder("triage", model)
.instructions("Classify the customer request.")
.build();Advantage: provider choice is a model construction concern. The agent, tools, pipeline, and calling code can stay the same when you change providers.
Typed Boundaries Over Hidden Magic
Tools use Zod schemas. Extractors use schemas. Pipelines preserve TypeScript input and output types across steps. Prompt responses, stream events, and observer events use explicit contracts.
The goal is not to remove all runtime errors. The goal is to make boundaries visible, testable, and hard to misuse casually.
import { ExtractorBuilder, PipelineBuilder, createTool } from "@anvia/core";
import { z } from "zod";
const ticketSchema = z.object({
customer: z.string(),
priority: z.enum(["low", "medium", "high"]),
summary: z.string(),
});
const lookupCustomer = createTool({
name: "lookup_customer",
input: z.object({ email: z.string().email() }),
execute: async ({ email }) => crm.lookup(email),
});
const extractor = new ExtractorBuilder(model, ticketSchema).build();
const pipeline = new PipelineBuilder<string>()
.step((input) => input.trim())
.extract(extractor)
.step((ticket) => ({
...ticket,
needsEscalation: ticket.priority === "high",
}))
.build();Advantage: the boundaries are visible in code. Tool input is validated before execution, extraction has a schema, and pipeline output types evolve step by step.
Composition Before Orchestration
Anvia favors small composable objects over a large orchestration layer. Agents can become tools. Pipelines can call agents and extractors. Tools can wrap application services. Studio can run already-built agents while you are still shaping the workflow.
This keeps simple workflows small while still allowing larger systems to be built from the same primitives.
const refundAgent = new AgentBuilder("refunds", model)
.description("Answers refund-policy questions.")
.instructions("Only answer refund-related questions.")
.build();
const supportAgent = new AgentBuilder("support", model)
.instructions("Route refund questions to the refund specialist.")
.tool(
refundAgent.asTool({
name: "ask_refund_agent",
maxTurns: 2,
}),
)
.build();
const workflow = new PipelineBuilder<string>()
.prompt(supportAgent)
.extract(extractor)
.build();Advantage: larger workflows are assembled from the same objects used in small workflows. You can test the refund agent, support agent, extractor, and pipeline separately.
Studio as an Iteration Runtime
Studio exists so you can test and inspect built agents before investing in product frontend, backend routes, internal dashboards, or approval consoles.
The design goal is not for Studio to replace your production application. The goal is to shorten the loop while the agent is still unstable: prompt it, inspect messages and tool calls, review traces, try sessions, and exercise approval metadata from a local UI.
import { Studio } from "@anvia/studio";
// Build the agent first. Use Studio to test it before building product UI.
new Studio([supportAgent]).start({ port: 3000 });Advantage: teams can focus on agent behavior first. Once the agent contract is stable, the same built agent can move into your route, job, queue worker, or product UI.
Optional Integrations
MCP, skills, retrieval, Chroma, Langfuse, and transformers are optional layers. They are useful when the application needs them, but they are not required to understand or run the core agent model.
The advantage is that teams can start with a single model call or agent and add retrieval, tools, tracing, pipelines, or local Studio testing incrementally.
import { langfuse } from "@anvia/langfuse";
const tracedAgent = new AgentBuilder("support", model)
.instructions("Answer support questions clearly.")
.observe(langfuse.create({ publicKey, secretKey }))
.build();Advantage: integrations add capabilities around the same runtime objects. You do not need to adopt Langfuse, retrieval, MCP, or a production UI before the basic agent model is useful.
Tradeoffs
| Design choice | Advantage | Tradeoff |
|---|---|---|
| Stable built objects | Easier testing, sharing, and reasoning | You wire objects explicitly |
| Application-owned infrastructure | Better fit for existing products and security boundaries | Less automatic platform behavior |
| Provider-agnostic models | Easier provider switching and mixed-provider systems | Some provider-specific features stay behind adapters |
| Typed tools, extractors, and pipelines | Clear contracts and safer composition | More upfront schema definition |
| Studio as an iteration runtime | Faster agent testing before product UI/API work | Studio is not your production product surface |
| Optional integrations | Smaller core and incremental adoption | You choose and configure the integrations you need |
Practical Rule
If a decision affects product correctness, security, or data ownership, keep it in application code or a tool. If a decision affects how a model is prompted, constrained, streamed, or observed, configure it on the agent or prompt request.
