oyadotai
v0.1.13
Published
A plan-don't-react framework for LLM agents. The model emits a typed dataflow plan once; the runtime executes it; the model never reads state it shouldn't.
Maintainers
Readme
oya
Agents that plan, don't react.
The model writes a typed plan once. The runtime runs it. Tool outputs never go back through the model - so your agent can't be prompt-injected through its tools, costs a fraction, and runs the same every time.
Drop-in for Mastra. TypeScript · Bun · MIT
Quickstart · The numbers · Why · Migrate from Mastra · Studio · White paper
The open-source core behind oya.ai - the hosted platform for plan-don't-react agents.
Quickstart
import { Agent, createTool } from "oyadotai";
import { anthropic } from "oyadotai/anthropic";
import { z } from "zod";
const getWeather = createTool({
id: "get_weather",
description: "Look up the weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city),
});
const agent = new Agent({
model: anthropic("claude-haiku-4-5-20251001"),
tools: { get_weather: getWeather },
});
const { text } = await agent.generate("How's the weather in NYC?");That's the whole API - the same shape as Mastra. Types are inferred from your
zod schema, and every value is OPAQUE to the model by default. You did less work
and it's injection-safe.
The numbers
Same task, same tools, real Anthropic API, 3 trials on claude-opus-4-7:
| | Vercel AI SDK | Mastra | oya | |---|--:|--:|--:| | total tokens | 3,653 | 9,143 | 1,783 | | model round-trips | 4 | 4 | 2 | | latency | 20.0s | 20.2s | 6.3s | | same plan every run? | no | no | yes |
½ the tokens of the leanest loop, 5× fewer than Mastra, ~3× faster - and
deterministic. Reproduce: bun run bench claude-opus-4-7. (The gap narrows on
smaller/cheaper models and widens with bigger payloads - full methodology and
numbers in benchmarks/.)
Why
Every other agent framework is a token loop (ReAct, LangGraph, AutoGen, Mastra, the Vercel AI SDK): the model picks a tool, sees the raw result, picks the next. Every URL, ID, and document flows back through the model. Three bugs follow - every time:
fetched: https://example.io/q3-report.pdf
downloaded: https://example.com/q3-report.pdf ← the model "fixed" the URLexpected: fetch → validate → download
observed: fetch → download → validate (skipped) ← the model reordered the steps$432 in tokens ← re-reading every result through the model
$51 in tokens ← reading only what it needs to decideSame root cause: the model read state it never needed to. oya makes that impossible. The model emits a typed dataflow plan; the runtime executes the DAG; each value is shown to the model only at the level the plan declares:
| level | the model sees | for |
|---|---|---|
| OPAQUE (default) | type + provenance - never the bytes | URLs, IDs, docs, payloads, secrets |
| SUMMARY | a bounded projection ({count}) | facts to branch on |
| TRANSPARENT | the full value | the user's message, the final answer |
An attacker can stuff a payload into any fetched page. The model never reads that
handle, so indirect prompt injection through tool output has nowhere to land. You
annotate none of this - it's OPAQUE by default.
Migrate from Mastra in 2 lines
If your app already uses @mastra/core, oya is close to a drop-in. createTool
and Agent mirror the shapes you already write, so for most apps the entire
migration is the import lines.
Step 1 - swap the imports:
- import { Agent } from "@mastra/core/agent";
- import { createTool } from "@mastra/core/tools";
- import { anthropic } from "@ai-sdk/anthropic";
+ import { Agent, createTool } from "oyadotai";
+ import { anthropic } from "oyadotai/anthropic";Step 2 - there is no step 2. Your tool and agent definitions compile unchanged.
What stays identical
| Mastra | oya | Notes |
|---|---|---|
| createTool({ id, description, inputSchema, execute }) | same | Types are inferred from the zod inputSchema; execute's argument is typed for you - no casts. |
| new Agent({ name, instructions, model, tools }) | same | tools is the same name → tool map. |
| await agent.generate(prompt) → { text } | same | text is the headline field, and token usage is reported for parity. |
| agent.stream(prompt) | same call | Returns structured events (fullStream / textStream) instead of a raw token soup. |
| anthropic(…) · openai(…) · google(…) | oyadotai/anthropic · …/openai · …/google | Same call shape, different import path. |
So the code you already have keeps working as-is:
import { Agent, createTool } from "oyadotai";
import { anthropic } from "oyadotai/anthropic";
import { z } from "zod";
const getWeather = createTool({
id: "get_weather",
description: "Look up the weather for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city),
});
const agent = new Agent({
name: "WeatherBot",
instructions: "Answer weather questions.",
model: anthropic("claude-haiku-4-5-20251001"),
tools: { get_weather: getWeather },
});
const { text } = await agent.generate("How's the weather in NYC?");What changes underneath
Same surface, different engine. Mastra runs a token loop: the model calls a
tool, reads the raw result, and decides the next call - re-reading every value back
through the model. oya has the model emit one typed plan and executes that DAG
directly. Values flow between tools by reference and are disclosed to the model
only at their declared projection level (OPAQUE by default).
You didn't touch your code, but the migrated app now gets, for free:
- ~½ the tokens of the leanest loop, 5× fewer than Mastra, ~3× faster (the numbers above) - intermediate state is piped tool-to-tool, never re-billed through the model.
- Deterministic execution - one statically-checked order on every run, instead of a model-chosen sequence that can reorder or skip steps.
- Injection-safe by construction - tool output the model never reads can't smuggle an injected instruction into its context.
Good to know before you migrate
- Projection defaults to
OPAQUE. Tool outputs are hidden from the model unless you mark themSUMMARYorTRANSPARENT. That's the safety win - but if a tool relied on the model reading its raw output to decide the next step, declare that output's projection. See Projection Types. - Providers import from
oyadotai/*(oyadotai/anthropic,oyadotai/openai,oyadotai/google) rather than@ai-sdk/*. - oya is pre-1.0 and mirrors the core of the Mastra surface, not every helper. If something you depend on is missing, open an issue - Mastra parity gaps are high-priority.
Studio
Chat with your agents and watch each plan execute live - the DAG, the trace, and
every value at its projection level (OPAQUE shows nothing, TRANSPARENT shows
the value). In your project:
// oya.config.ts
export default { agents: { support } };bunx oyadotai dev # → oya Studio at localhost:4000Use it anywhere
oya is a library, not a platform. Run an agent in a script, a Next.js route, a
Bun server, a worker, the edge - await agent.generate(prompt). Stream it with
agent.stream(prompt) (structured events, not a token soup) and render it with
oya/react's usePlan / useChat, or serve SSE with oyadotai-server.
Built on this: oya.ai
This package is the open-source core that oya.ai runs on - the same plan-once runtime, projection types, and Studio, now hosted. Want the managed platform (deploy agents, schedules, skills, and the Studio without running any infrastructure) instead of wiring it up yourself? Start at oya.ai - everything you build against this library is the same engine that powers it.
Install
bun add oyadotai zodPackages
| package | what |
|---|---|
| oyadotai | the runtime + Agent + createTool; oyadotai/anthropic · oyadotai/openai · oyadotai/google providers; oyadotai/react hooks; the oya dev studio |
| oyadotai-server | toSSEResponse / toTextResponse for any Fetch server |
| @oya/playground | the Next.js studio (make dev) |
| @oya/benchmarks | the live comparison above |
Develop
make dev # oya Studio
make test # bun:test - checked against the Python reference runtime
make bench # the comparison
make check # typecheck + test, every packageCommunity & contributing
oya is a community project - contributions of every size are welcome.
- Contributing guide · Good first issues
- Discussions · Code of Conduct · Governance
- Security issues: report privately per SECURITY.md. Anything else: [email protected].
White paper
oya is the TypeScript implementation of Plan, Don't React: Projection Types for LLM Agent Runtimes. Read the white paper:
License
MIT © Oya Labs, Inc.
