I Wanted My Own Agent Library, So I Built One

I Wanted My Own Agent Library, So I Built One

By Yuriy Zhar 7 min read
The first article in the Spectre series introduces Spectre Kinetic, an Elixir-first planning layer for agents that lets models express intent while the runtime keeps control.

I did not wake up one morning and think, “You know what the world needs? Another agent framework.” I really did not.

What I wanted was something smaller. Something more personal. Something that matched the way I build systems in Elixir, not the way most agent demos are shown on stage.

I like agents, but I dont like when the agent becomes this giant magical object that owns everything. The model should help express intent. It should not become the runtime, the policy engine, the permission system, and the execution layer all at once.

Every Agent Library Has Its Own Opinion

After you spend some time around agent libraries, you notice that each one comes with its own worldview.

Some want everything to be a graph. Some want everything to be a chain. Some want everything to be a workflow. Some want your whole application to become a collection of JSON schemas and tool descriptions.

And honestly, that is fine. Different libraries solve different problems. Different people have different pain. One developer needs orchestration. Another needs memory. Another needs browser automation. Another just wants the model to call one function without turning the project into a protocol museum.

For me, the problem was simple: I wanted small Elixir libraries that treat agents like production systems, not like magical interns with API keys.

This article is the first piece of that series. The Spectre series.

It starts with Spectre Kinetic, because before an agent remembers anything, browses anything, or tries to reason about your application, it needs to answer a very basic question:

How should it use tools?

The MCP Decision Is Not Always The Right Fit

I want to be careful here, because MCP is not useless. It makes sense when you need an external protocol. Remote tools, shared integrations, separate systems, a standard bridge between things that do not know each other. For that, yes, I understand why it exists.

But inside your own application, the situation is different.

Your tools already live there. Your runtime already knows the modules, permissions, types, retries, supervision, and side effects. Your application already has rules about what should happen and what should never happen.

So using a full external protocol inside your own app can start to feel heavy. Sometimes it is the right choice. Many times, for me at least, it feels like too much ceremony around something that should be local and inspectable.

The strange thing is that language models are already good at language. They can read messy human intent and produce a compact action-like expression. But then we often wrap that in more schemas, more transport, more ceremony, more moving pieces.

The model should not hold the keys. It should fill out the request form.

That is the boundary I care about.

The model should not be the runtime. It should not be trusted to know what is safe, what exists, what is allowed, and what should happen next. It can propose. The application should decide.

Let The Model Do What It Is Good At

A language model is good at language. This sounds obvious, but in agent systems we often forget it.

The model can take something like “tell ops the deploy failed and include the log link” and turn it into something close to “send email to ops with this body”. That is usefull. That is the part where the model earns its GPU bill.

But it should not become your execution environment.

It should not decide if the email module exists. It should not decide if the recipient is allowed. It should not decide if the body is complete. It should not decide if the action is safe. And it should definetly not decide if your database can be dropped because some prompt was written in a dramatic way.

In theory, you give the agent tools and it uses them. In practice, every tool call has more inside it than it seems. There is intent, arguments, missing fields, policy risk, confidence, side effects, and consequences.

A tool call is not really an action yet. It is an intention that still needs to be inspected.

So the split I wanted was simple:

Let the model generate an English-like action intent. Let Elixir inspect that intent. Let the runtime map it to real functions. Let the application decide what is allowed to happen.

The model proposes. The runtime checks. OTP supervises. And when something fails, at least it fails in a place where we can log it, test it, and understand it.

That Is Where Spectre Kinetic Enters

Spectre Kinetic is not trying to be the agent. That is important.

It does not want to orchestrate your whole life, execute side effects, invent workflows, or become a tiny god inside your Phoenix app.

It sits between model intent and Elixir execution. That is where I think this kind of library should sit.

You define tools in Elixir. Real modules. Real functions. Real parameter names. Real typespecs.

Then Kinetic extracts tool knowledge into a registry. The model does not need to carry a giant live catalog of schemas in the context window. It can emit compact Action Language instead, and Kinetic can map that language to functions your application already owns.

The part I like most is that the tool can be described close to the real function.

The function stays a function. The examples live near the function. The agent learns the shape of the action from the same place a human developer would look.

defmodule MyApp.Emailer do
  use SpectreKinetic

  @al ~s(SEND EMAIL TO=email@gmail.com BODY=text)

  @doc """
  Send an email to a recipient.

  AL: SEND EMAIL TO="dev@example.com" BODY="hello"
  AL: SEND MAIL TO="ops@example.com" BODY="pager"
  """

  @spec send(email :: String.t(), text :: String.t()) ::
          {:ok, String.t()} | {:error, term()}
  def send(email, text) do
    {:ok, "#{email}:#{text}"}
  end
end

This is the feature that made the idea click for me.

The action examples are not floating somewhere in a separate prompt file. They sit beside the function they describe. The function has a spec. The docs contain real Action Language examples. Kinetic can read that and build planning knowledge from something you already maintain.

For example, the user says something like “send mail to ops@example.com body pager”. Kinetic can plan that into a structured action candidate pointing at your email function, with mapped arguments, status, scores, warnings, and missing fields when something is incomplete.

Then it stops.

That part matters. It does not execute the function by itself. Execution belongs to your app, where permissions, transactions, retries, audit logs, and confirmation flows already live.

This is the boundary I wanted: imagination on one side, consequences on the other.

The Runtime Should Stay Calm Even When The Model Is Dramatic

The best thing about Spectre Kinetic is not that it makes agents more powerful. Making something powerful is easy. Making it controlled is the hard part.

Kinetic makes the boundary visible. It turns vague model intent into something your Elixir application can inspect.

You can score tool matches. You can map slots. You can detect missing arguments. You can add classifier plugs for confidence, slot quality, safety, or custom policy.

You can say: this plan looks fine. This needs clarification. This should be blocked. This needs a human confirmation. This action is not allowed in this scope.

Thats the reason I started building Spectre as a series of libraries instead of buying fully into one big agent framework.

Agents need different pieces. Planning is not memory. Memory is not perception. Perception is not execution. When a framework blends everything into one abstraction, it can feel good at the beginning, but later it becomes harder to know where a decision actually happened.

Spectre Kinetic is the planning piece.

Small, local, Elixir-first, and suspicious of drama.

The agent gets to be expressive. Elixir gets to be boring. And boring, in production, is not an insult. Boring is the system returning the same result twice. Boring is logs, supervision, explicit boundaries, and code you can reason about after midnight.

That is what I want from agent tooling. Not magic. Not a framework religion. Just a clean place where model intent becomes something the application can inspect before anything real happens.

Send via.chat
Recommended Tool

Send via.chat

Receive form leads, send login codes, and route important alerts through WhatsApp or Telegram.

Share this article:
Yuriy Zhar

Yuriy Zhar

github.com

Passionate web developer. Love Elixir/Erlang, Go, TypeScript, Svelte. Interested in ML, LLM, astronomy, philosophy. Enjoy traveling and napping.

Get in Touch

Have a question or want to work together? Drop a message below.

Book a Call

Stay updated

Subscribe to our newsletter and get the latest articles delivered to your inbox.