I am building Anvia because I care a lot about DX.

For a developer tool, DX is the product experience. It is how the library feels when an engineer uses it. The API is the interface. The abstraction is the interface. The way objects compose is the interface.

That is why I enjoy building lower-level libraries for engineers.

Lower-level does not mean rough. It does not mean unclear. It should still feel designed. A good lower-level library gives engineers control, but it also gives them a clean mental model.

That is the space I want Anvia to live in.

The abstraction did not fit what I wanted

There are already many good tools for building AI applications.

Some tools are centered around UI. Some are centered around a provider runtime. Some are centered around graph orchestration. Some try to become a larger application framework.

Those directions are valid, but they were not exactly what I wanted.

I wanted an AI runtime that fits inside an existing TypeScript application.

I do not want the agent framework to own my whole product. I do not want it to decide how my auth, database, queues, permissions, memory, audit logs, deployment, or side effects should work.

Those parts should stay in the application.

What I wanted was a runtime layer for the AI behavior itself: models, agents, tools, structured output, streaming, hooks, retrieval, pipelines, observers, and local inspection.

That boundary is the main idea behind Anvia.

The application owns the product.

Anvia owns the AI runtime boundary.

Explicit TypeScript objects

One thing I care about in Anvia is that AI behavior should be represented as explicit TypeScript objects.

An agent should not feel like something hidden inside a framework container. It should be something I can create, configure, import, pass into a route, run inside a job, test, or inspect locally.

The same applies to tools, extractors, models, and pipelines.

This sounds simple, but it matters a lot.

When behavior is an explicit object, the system becomes easier to reason about. I can see what model the agent uses. I can see what instructions it has. I can see what tools are available. I can see where hooks and observers are attached.

There is less magic.

And for production software, less magic is often better DX.

This is the kind of shape I want:

// app/ai.ts
import { Agent } from '@anvia/core'
import { OpenAIClient } from '@anvia/openai'

const apiKey = process.env.OPENAI_API_KEY

if (!apiKey) {
  throw new Error('OPENAI_API_KEY is required')
}

const client = new OpenAIClient({ apiKey })
const model = client.completionModel({
  modelId: 'gpt-5.6-sol',
  api: 'responses',
})

export const supportAgent = new Agent({
  id: 'support',
  name: 'Support Agent',
  model,
  instructions: 'Answer support questions clearly.',
  maxTurns: 3,
})

The agent is not hidden. It is a value. I can import it wherever the application needs it.

// app/routes/support.ts
import { supportAgent } from '../ai'

export async function POST(request: Request) {
  const { message } = await request.json()
  const result = await supportAgent.generate({ prompt: message })

  if (result.type === 'interaction') {
    return Response.json(
      { error: `Interaction required: ${result.interaction.type}` },
      { status: 409 },
    )
  }

  if (result.type === 'blocked') {
    return Response.json(
      { error: result.reason, stage: result.stage },
      { status: 422 },
    )
  }

  return Response.json({
    output: result.output,
    messages: result.messages,
    usage: result.usage,
  })
}

Stable defaults, request-time control

Another design detail I care about is separating stable behavior from request-specific behavior.

Stable behavior belongs on the agent: identity, instructions, default model options, tools, context, output schema, observers, and default limits.

Request-specific behavior belongs on the run request: the current input and one-off limits such as max turns.

This distinction keeps the agent reusable.

You can create an agent once, wire it near your application startup, and reuse it across routes, jobs, tests, or Studio. Then each run can still carry the things that only belong to that request.

That is the kind of API design I like. Not because it is clever, but because it gives the engineer a clear place to put things.

Good DX is often just good placement.

For example, the agent can own stable defaults, while a single request can still override what belongs only to that run:

const result = await supportAgent.generate({
  prompt: 'Summarize this support ticket.',
  maxTurns: 1,
})

if (result.type === 'response') {
  console.log(result.output)
}

That distinction is small, but it makes the mental model cleaner.

Composition over a big runtime

I do not want Anvia to feel like a giant system that forces everything through one shape.

I want small primitives that compose:

  • Agents can use tools.
  • Agents can become tools.
  • Pipelines can call agents and extractors.
  • Tools can wrap application services.
  • Hooks can control execution.
  • Observers can inspect what happened.
  • Studio can run the same built agents during local iteration.

This is important because real applications do not all have the same shape.

Some teams need a single support agent. Some need structured extraction. Some need retrieval. Some need multiple specialist agents. Some need approval flows. Some need tracing and evals.

I want Anvia to let engineers start small and add pieces when the application actually needs them.

The primitives should compose without becoming a new application framework:

import { Agent } from '@anvia/core'

const refundAgent = new Agent({
  id: 'refunds',
  model,
  instructions: 'Only answer refund-policy questions.',
  maxTurns: 2,
})

const askRefundAgent = refundAgent.asTool({
  name: 'ask_refund_agent',
  description: 'Ask the refund specialist about refund policy.',
  maxTurns: 2,
  suspension: 'reject',
})

const supportAgent = new Agent({
  id: 'support',
  model,
  instructions: 'Route refund questions to the refund specialist.',
  maxTurns: 4,
  tools: [askRefundAgent],
})

An agent can be a normal agent, or it can become a tool for another agent. That is the kind of composability I want in a lower-level runtime.

Application-owned infrastructure

The strongest design boundary in Anvia is application-owned infrastructure.

Anvia should not be the source of truth for your users, auth, permissions, database, queue, audit logs, billing, or deployment.

Those decisions are too close to product correctness and security.

A tool can call your application service. A protected tool can suspend before execution so your approval system can decide. A lifecycle observer can send traces to the system you already use.

But the product still owns the product behavior.

This is the part I was missing from many abstractions. I want the AI runtime to be powerful, but I do not want it to take over the application.

That boundary also shows up in tools and approval interactions:

import { Agent, createTool } from '@anvia/core'
import { z } from 'zod'

const refundOrder = createTool({
  name: 'refund_order',
  description: 'Refund a paid order.',
  inputSchema: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
  }),
  requiresApproval: ({ orderId, amount }) => ({
    reason: `Approve a ${amount} refund for ${orderId}`,
  }),
  async execute({ orderId, amount }) {
    return billing.refund({ orderId, amount })
  },
})

const refundsAgent = new Agent({
  id: 'refunds',
  model,
  instructions: 'Use refund_order only after checking eligibility.',
  tools: [refundOrder],
})

let result = await refundsAgent.generate({
  prompt: 'Refund order A-100 for 25 dollars.',
})

if (result.type === 'interaction' && result.interaction.type === 'tool-approval') {
  const decision = await approvals.waitForDecision({
    interactionId: result.interaction.id,
    toolName: result.interaction.toolName,
    input: result.interaction.input,
    reason: result.interaction.reason,
  })

  result = await refundsAgent.resume(result.continuation, {
    type: 'tool-approval',
    approved: decision.approved,
    reason: decision.reason,
  })
}

if (result.type === 'response') {
  console.log(result.output)
}

Anvia gives the suspension and continuation boundary, but the approval system, billing service, audit log, and permission model still belong to the application.

Why I care about this

I am passionate about this kind of work because abstraction is design.

A good library changes how engineers think. It gives them names for concepts. It gives them boundaries. It helps them make decisions without hiding the system from them.

That is the kind of developer tool I want to build.

Anvia is not just another wrapper around model APIs. It is my attempt to design the agent runtime I wanted to use myself:

  • Lower-level, but polished.
  • Explicit, but still ergonomic.
  • Flexible, but not vague.
  • TypeScript-native, but not locked into one product architecture.
  • Powerful, but still application-owned.

That is why I am building Anvia.