npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@idealic/agent

v0.1.0

Published

A schema-driven agent loop: tools, activities, delegates, plans and observations.

Readme

@idealic/agent

For a developer deciding whether this library does what they need, and looking for the door. Evergreen — every number and date lives in the measurement pages this links to, never here.

This is an agent runtime where the plan is data and the wiring is a language. A model does not call one tool and wait; it authors a graph of calls whose edges are the paths they read and write, and the loop runs whatever the data already permits. A parameter can hold a computation instead of a lookup. A condition can be a named test the whole run is checked against. A step can have no code behind it at all, and the model owes the answer.

If you want a chat loop with function calling, almost anything will do and this will feel heavy. If you want a run that can branch, fan out over many subjects, hold work behind an approval, and tell you what it spent — this is what it is built for.

Everything below is a capability, with the caveat that would otherwise bite you attached to it. The normative rule for each lives in an Act; the door to each Act is on the line that describes it.


What this is, and what it is not

It is a protocol implementation. The Acts of Emergence define the architecture — a request, a tool, an activity, a call, a plan, a claim — and this library is the reference runtime for them. Each Act is a discrete capability you opt into: simple tool use costs you almost nothing of the rest.

It is not a prompt-engineering toolkit, and it is not batteries-included. There is no memory store, no retriever, no built-in vector database, no hosted anything. It talks to model providers, runs your code, and keeps the run's own values straight.

It is not a validator of what a model returns. The loop parses the response and hands it on. Some providers enforce your schema and some declare no schema support at all, and against those the answer is checked against nothing. Read the provider table before you trust a shape.


The loop: what one turn is

One turn is one request to the model. The model answers with an output — your schema, wrapped so the loop can tell "finished" from "still working" — and a list of calls.

While the response is still streaming, the loop drains those calls. It asks which of them the context already satisfies, launches all of those at once, and rescans every time a completed call widens the context. A call whose parameter reads a path nobody has written yet simply waits; it is not scheduled and then blocked, it is never offered.

  • A call that throws does not end the run. The failure is collected and appended as an error message, and the next request reads it. Self-correction is the model reading its own failures, not a retry policy.
  • A turn ends when the drain is empty and the stream has closed. Everything the turn produced goes back through a carrier that asks each kind of content what its value becomes in the next context.
  • The run ends when a solution carries a non-null output, or when the turns run out.

There is no if anywhere in this. A branch is a call whose Output Path declares two outcomes; exactly one is written, and the call reading the other never becomes startable. The graph is a constant and the execution is a function of the data.

010: Agent/Loop · 012: Agent/Plan


Expressions — a parameter that computes rather than looks up

A Variable Reference (†state.total) names a value the run will hold, addressed before it exists. An Expression is the general form: a parameter holding a computation over those references, written in the language you already know.

†state.items |> filter(i => i.paid) |> map(i => i.cents, 4) |> reduce((a, b) => a + b, 0)

References are resolved before the body runs, so array methods, optional chaining and comparison all work without having been designed for. Each reference is rewritten to a generated identifier and passed in as an argument — never spliced into the source as text.

Three operators, and why they are not the language's own

|| and ?? test truthiness. These test existence, and a reference that has not resolved is not null — it is a thing still pending. Same characters, different answer, nothing to notice. So the operators are ones JavaScript cannot parse, which means they can never be quietly read as the language's own:

| operator | name | meaning | |---|---|---| | <\|> | choose | the first alternative that has a value | | *> | gate | wait for the left, discard it, take the right | | \|> | flow | x \|> f is f(x) |

Precedence, tightest to loosest: <|>, then |>, then *>. Parenthesise whenever you mix them.

<|> is left-preferred, not a race. This is the sentence most likely to be got wrong. The value read is the left whenever the left has one. What makes it speculative is readiness: the call fires as soon as either side has landed. Two different questions, and only the first decides the value.

Nothing is cancelled. When the left answers, the right is simply never evaluated — nothing was rescinded and no work was thrown away. And where a losing arm did run, it ran: only its value stops travelling. A losing arm that awaited a side-effecting tool has still called it.

Write the curried form

†state.items |> map(f, 4) is right. map(†state.items, f) also computes the correct answer — and silently drops the run's abort signal, so the work becomes uncancellable. The degradation is loss of cancellation, never a wrong value, which is exactly why nothing warns.

Where a sigil may appear

Only at the top level of an expression. A Variable Reference inside a callback is fine — filter(p => †state.chosen.includes(p.id)) is the idiom — but an operator nested inside one is not.

A pipeline welded into a larger expression becomes literal text, silently. 'total: ' + (†state.x |> f) puts the |> at depth one, so nothing cuts it, nothing compiles it, and the activity is handed the dagger string itself. There is no error anywhere. Keep the pipeline as the whole parameter, and put the varying part inside a callback.

The model does not reach for this unprompted

Given a goal that states only the job, a model will read the numbers and add them up by hand rather than write an expression. It is not shy about the mechanism — it fills a latent _output readily — it simply never computes in a parameter unless told to.

The standing instruction is what teaches it. That block ships only on a turn that already carries a named claim, so a run that declares none never sees any of this prose and will never use the layer. If you want the pipeline, declare a claim — a trivially true one is enough — or ship the instruction yourself.

007: Agent/Variables · 011: Agent/Expressions


Claims — named tests over the run's own values

A claim is an expression given a name and kept for the length of the run. Its value lands at †expr.<name> and is read like any other reference.

{ "type": "expressions", "expressions": [
  { "name": "isPolicy",   "expression": "†state.kind === 'policy'" },
  { "name": "wellFormed", "expression": "†state.result?.effectiveDate != null && †state.result.limits > 0" }
]}

A call that reads a claim both takes its value and waits for it to be true. That is the whole branch mechanism: two calls reading claims that cannot both hold are two arms, authored in one turn, decided without spending one.

Global by declaration, per instance by evaluation. One declaration, evaluated separately for each subject it is read against — instance ②'s values when ②'s call is being considered, ③'s when it is ③'s. So the same claim can hold for one subject and fail for another, and two subjects handed the same plan can end differently. Nothing is copied per subject and nothing needs to be aimed.

Three things to know before you use one:

  • A claim a turn declares does not gate that turn's own calls. Declarations reach the context through the carrier, which runs after the drain — so during the turn that authored it, the name has never been heard, and an unheard claim is permissive. Both arms run. A branch on a value produced this turn goes in an inline _when; a named claim is for a condition already standing, or for the turns after the one that declared it.
  • Nothing retires a claim. Once declared, a name is checked for the rest of the run. Leaving it out of a later turn changes nothing — it stays in force exactly as it was. The nearest thing to withdrawal is redeclaring it with an expression that always holds.
  • A negation cannot be the other arm. _when: "!†expr.wellFormed" reads wellFormed, and reading means waiting for it to become true — so that arm never runs and nothing says so. Declare two claims, each true in its own case.

A claim that throws, times out, will not compile, or names something nothing declared behaves as though it were absent: the call is not held, and the failure is reported once. Broken is the absence of an answer, not a third verdict — which is what makes it impossible for a broken claim to deadlock a graph.

095: Agent/Claims


Latent execution — a judgement with no tool behind it

Register a Tool with no matching Activity and its _activity resolves to the empty string: the model owes the _output itself. Nothing was implemented, and the step still runs.

This is not only a prototyping convenience. A latent step is how you get a judgement — which of these read as positive, which of these is the endorsement — into a run as a value rather than as prose. And it settles on the turn that authors it: the model fills _output in the same response that carries the call, the write lands in that turn's drain, and a call whose expression reads that path runs on the same turn. One request, a judgement made and everything downstream of it computed.

{ "calls": [
  { "_tool": "judgeTone", "_activity": "", "_outputPath": "†state.judgement",
    "_output": { "positive": ["c1", "c4"] } },
  { "_tool": "fileDigest", "_outputPath": "†state.filed",
    "words": "†state.passages |> filter(p => †state.judgement.positive.includes(p.id)) |> map(p => p.words, 4) |> reduce((a, n) => a + n, 0)" }
]}

Crystallise later: register an Activity under the same name and the Tool's interface does not move. Nothing that called it changes.

003: Agent/Activity · 104: Concept/Latent


Instancing — one plan aimed at several subjects

Stamp _instance on a call and it belongs to that subject. The context groups by it, references resolve within it, and writes land inside it. Input folds global first, then instance, so a subject's own value overrides the shared one however old the shared one is.

The plan itself says nothing about instances. It fans out because the aiming rides on each call, and claims fan out for the same reason one level up: the aiming rides on each reading.

013: Agent/Instancing


Plans and proposals

The calls a turn emits become the next turn's plan, unchanged — the same shape a model emits, an author writes by hand, and the context renders back. There is nothing in between to translate, so the forms cannot drift apart.

A proposal is a step held behind an approval path: a call whose condition reads under †state.approvals, which nothing outside the run can write while the run is turning. It waits, the run ends, and the host is told once what it ended waiting for. When a call the run itself authored wrote the gate, that is reported too, marked as self-approved rather than pruned — the host is the only party that can tell a scripted approval from one the run granted itself.

012: Agent/Plan


The stages behind |>

map, filter, reduce, take and their siblings in a pipeline are not the array methods — they are @idealic/iterators, bound into the expression already carrying the evaluation's own abort signal. Three things follow that an array chain cannot do:

  • Concurrency is an argument. map(enrich, 4).
  • take stops the work rather than discarding it. |> map(expensive) |> take(2) runs expensive twice. Put take before the stage that can throw or that costs; after a concurrent stage, prefetch can already have reached what the bound excludes.
  • The run can cancel. A pipeline that exceeds its budget is told to stop instead of running on unobserved.

A pipeline ending in a streaming operator reaches the parameter as an array; one ending in reduce, find or every answers with that value directly.

Nothing an expression callback reaches is asynchronous. The values are already in hand by the time the body runs. So a concurrency argument overlaps nothing inside an ordinary run, and it is only worth its keep against a stage that genuinely waits.

The authoring vocabulary is deliberately withheld from expressions — you cannot rebind the signal from inside one, which is the guarantee the scope exists to hold.


Hooks — two verbs, and the difference matters

One registration primitive, two verbs, and the power gap between them is the point.

  • addInterceptor puts you in the path. You receive the arguments and return them, so you may replace any of them, and a throw fails the operation. This is how the API key gets injected — swallowing a failure to inject one would send the request without it.
  • addObserver puts you beside the path. Nothing you return goes anywhere, and a throw is swallowed. That safety is the entire reason for the second verb: an observer is meant to be added liberally, from code with no business affecting the run it watches.

Request takes interceptors. Usage, Agent and the expression evaluator take observers, and everything registered returns the hook, so an inline function is still removable.

The evaluator observer is the one worth knowing about. It reports which arm of a <|> supplied the value — in the author's own words, not the compiled identifiers — and separately why a call became ready when it did. Note two things: it says which arm supplied the value, never which arrived first, because there is no such fact; and readiness fires per ask, not per transition, so dedupe on your side.

With no observer registered the cost is one boolean check. Payloads are built only after something says it is listening, and the both-sides walk that readiness reporting needs is only done under observation.


Usage and cost

Every provider reports what a turn cost in its own vocabulary. Usage normalises them into one shape, so comparing two providers compares quantities rather than vendor field names.

null is not zero, everywhere in this. A count is null when the provider did not report it. cachedPromptTokens: null says the response carried no cache field at all — a different statement from a reported cache of zero, and rendering them the same turns "we never measured this" into "we measured no caching".

Reasoning tokens need a second fact to be billable at all. The OpenAI shape nests thinking inside the completion count; Gemini reports it as a third sibling. Adding it on the first double-counts; not adding it on the second understates by however much the model thought — and on a thinking turn that is not a rounding error. So the shape carries completionIncludesReasoning, and a turn that reports thinking without saying which convention produced it is not costed, rather than guessed.

Cost is a pure function from those counts and a provider rate to dollars. What it refuses to do is the useful part:

  • An unknown model returns usd: null, never $0. A total that quietly omits an unpriced model reads as a cheap run, which is the failure the whole shape exists against.
  • Every gap is in the return value, not in a log line: which dimension could not be priced and why. A turn with an undecidable output still contributes its priced input half and says so.
  • Absence is spelled out. A charge the vendor does not levy, a charge nobody has read, a model somebody looked up and found retired, and a model nobody looked up are four different statements — and none of them is zero.
  • A rate carries its source and the date it was read, or it does not go in the table. A rate without a source is a guess with a decimal point. Which providers and models actually carry numbers changes; read the tables in src/Provider/.

092: Agent/Limits · 091: Agent/Caching


Completeness — a truncated turn is not an empty answer

A model that ran out of budget mid-answer used to reach the caller as {} — indistinguishable from a turn that legitimately answered nothing, which the loop reads as keep going.

Now the parser decides, not the finish reason. A turn capped at MAX_TOKENS whose JSON nonetheless closed is complete; the reason rides along beside the answer and is never acted on. A turn whose JSON did not close rejects, carrying the parsed prefix, the raw text, the provider's own finish reason, and every candidate — so an n > 1 sibling that did finish is not lost with the one that did not.

It rejects rather than returning the prefix for a specific reason: a truncated string arrives as a plausible-looking value in the right slot, and handing that back would make the loop treat half a sentence as the final answer. A caller who wants the old leniency writes one catch and reads the partial.

A tool-call-only turn and a turn that produced no characters are told apart, not collapsed.

001: Agent/Request


Providers

Each provider module declares three tables next to each other: the models it can address, the features it supports, and — where somebody has read the vendor's page — the rates those models bill at. Gemini, Vertex AI, OpenAI, OpenRouter, DeepSeek, Groq, Cerebras, xAI and a local llama route each have one.

The feature table is the one to read first, and schema is its most consequential row. Some providers enforce your response schema; several declare no schema support, and against those nothing checks what comes back. Others differ in the opposite direction on optionality, nesting depth, property counts, and which keywords are silently dropped rather than rejected.

Those differences are not folklore. They were measured against real endpoints, and each reading is written down with its date, its model, and what it does not establish — docs/measurements/.

094: Agent/Config · 090: Agent/Typing


Limits — what ships, and what is designed

maxTurns ships, and it ships with the part that is easy to get wrong. One turn before the cap the loop appends an exhaustion notice to the context, so the last request carries the news that it is the last — and the turn that reads it is still able to act on it. When the cap arrives the run returns nothing rather than throwing, because a structural failure in this system is a message the next request reads, not an exception.

The other dimensions are a design, not a description. Wall clock, tokens, money, and cache held against a future wake are specified in the Act and are not callable. So is the visibility half — a run today cannot see what it has left or what it has spent, which is the difference between a cap and a budget. Do not read that Act as a feature list.

092: Agent/Limits


Agent, Request, and the shape they share

A Request is one atomic transaction with a model: no loop, exactly one generation step. It supports multiplexing — several candidate solutions from one context — and a callbackPath that hands you parts of the structure as they stream, which is how tool calls start running while the rest of the answer is still arriving.

An Agent is the loop around Requests.

Both normalise their result the same way, so there is no duality to keep in your head: your schema is always wrapped into an output property. A Request returns an array of solutions; an Agent returns the one it converged on. The loop's termination condition is that property going non-null.

001: Agent/Request


Types

Types come from your schemas through Schemistry, with no code generation step. Tools register their interface; Activities register their implementation; both are inferred from the schema rather than restated.

The part that is intended rather than finished is making the type pipeline mirror the runtime one. The schema a provider actually receives is a composition — your schema plus whatever the active content handlers injected (calls, advisors, meta) — and the goal is for each handler to transform the inferred type exactly as it transforms the runtime schema, so the final type is the strict intersection of your intent and the system's capabilities. Treat that as the direction, not as a guarantee you can lean on today.

090: Agent/Typing


Extending it

  • Tools register an interface; Activities register the code behind it. The same name binds them by convention; no Activity means latent.
  • Content handlers are the extension point that reaches the wire: a handler receives the config, schema, messages and callback and returns the same shape, so it can inject a schema fragment, seat a message, or change a setting.
  • Presets bundle messages, transformers and settings under a name, invoked declaratively in the message stream — a council of advisors, a persona, a game player.
  • Schemas register once and are referenced by $ref.

Where the rules and the numbers live

Three places, and mixing them is what makes documentation rot:

  • The Acts — normative and evergreen. What the system commits to. 801: Package/Agent is the act-by-act catalogue of this library; start there when you want the rule rather than the shape.
  • docs/measurements/ — dated by construction. What a provider actually did, on a date, against one endpoint and one model, with what the reading does not establish stated beside it.
  • This page — what the library can do and where the sharp edges are.

A rule goes in an Act. A number goes in measurements. This points at both.