Agent primitives
When your API has AI-shaped endpoints, Glotto promotes those shapes into behavior: the generated SDK ships a small cohort of helpers for the work every agent loop repeats — accumulating streamed tool calls, counting tokens, budgeting a context window, comparing embeddings, pulling a JSON object out of model prose, and retrying without blowing the budget.
They are part of the SDK, not a companion library. Every helper is pure, dependency-free, deterministic and never-throwing, with identical semantics in all 13 languages — no runtime to install, no vendor data table, nothing to keep in sync.
What your SDK emits
The cohort is gated per family, on the shape of your API’s responses — so you get the helpers that make sense for the endpoints you actually have:
| Helper | Emitted when your API has |
|---|---|
ToolCallAccumulator |
an SSE streaming operation |
estimateTokens, Tokenizer, TokenBudget, RetryBudget |
a response carrying a token-usage shape (a usage object, or token-named numeric fields) |
VectorMath, NearestMatch |
a response carrying an embedding vector (embedding / embeddings / vector) |
StructuredOutput |
any of the above |
Streaming is not a precondition. A non-streaming API whose responses report token usage emits
the budgeting helpers with no SSE runtime at all; a non-streaming embeddings API emits the vector
helpers the same way. Only ToolCallAccumulator is tied to SSE. Conversely, an API with no AI
shape at all emits none of them, and its generated SDK is byte-identical to one generated before
these helpers existed.
Counting tokens
Two helpers, deliberately: a cheap estimate and a real tokenizer.
estimateTokens('hello world'); // 3 — chars/4 heuristic, O(1) work
Tokenizer.countTokens('hello world'); // 2 — character-class pre-tokenizer + subword model
estimateTokens is the flat ≈4-characters-per-token approximation. Reach for it when you want a
rough number in a hot loop and precision does not matter.
Tokenizer.countTokens(text) is the one to use when the number drives a decision — whether the
next message fits, how much history to trim. It is a real tokenization algorithm: a regex-free
character-class pre-tokenizer that groups letters, digits, whitespace and symbols into runs and
costs each run with a calibrated subword model. That puts it materially closer to a real BPE count
than chars/4, while shipping no vocabulary file — nothing model-family-specific to download,
version, or keep current.
It never throws (empty input is 0) and it is deterministic — the same string always costs the
same, in every language.
It is an estimate, not a billing oracle. If you need counts that match a specific vendor’s tokenizer exactly, call that vendor’s tokenizer;
countTokensexists so you don’t need a dependency to make a sizing decision.
Budgeting a context window
TokenBudget does exact accounting against a ceiling you set, seeded from what the wire actually
reported:
const budget = new TokenBudget(128_000);
budget.add(response.usage.total_tokens); // seed from the response
budget.remaining(); // 128_000 − consumed, clamped at 0
budget.fits(4_000); // does the next call still fit?
budget.exceeded(); // over the ceiling?
Pair it with the tokenizer to decide before you spend: budget.fits(Tokenizer.countTokens(prompt)).
Budget-aware retry
RetryBudget composes TokenBudget, so a retry loop stops when either the attempt count or
the remaining tokens are exhausted — the failure mode where a loop retries itself out of context
while still under its attempt limit:
const retry = new RetryBudget(3, budget);
if (retry.shouldRetry(attempt, Tokenizer.countTokens(prompt))) {
await sleep(retry.nextDelayMs(attempt, 500, 30_000));
}
Backoff is deterministic and capped — same inputs, same delay, so a retry path is testable.
Embeddings and vectors
VectorMath covers the retrieval-loop math, so a similarity search doesn’t pull in a numerics
library:
const hits = VectorMath.topKNearest(queryEmbedding, docEmbeddings, 5);
// [{ index, score }, …] — sorted by score, ties broken by index
Also cosineSimilarity, dotProduct, magnitude and normalize. Mismatched, empty or
zero-magnitude vectors degrade to 0.0 rather than throwing — a ranking loop over ragged data
returns a usable ordering instead of a stack trace.
Structured output
Models return JSON wrapped in prose, or in a fenced block, or truncated. StructuredOutput.decode
pulls the first balanced JSON object out and parses it:
const { ok, value, json } = StructuredOutput.decode(message.content);
if (ok) use(value); // parsed object; otherwise inspect the raw `json`
It never throws. When the model returns malformed or partial JSON, ok is false and the raw
bytes stay on json so you can log, repair or retry.
Accumulating streaming tool-call deltas
ToolCallAccumulator reassembles a tool call whose arguments arrive as partial JSON fragments
across many SSE events. Because it is tied to the streaming runtime, it is documented with the rest
of streaming — see Streaming.
Naming in your language
Each engine spells the cohort its own way while keeping the semantics identical — countTokens in
TypeScript and React Native, count_tokens in Python and Ruby, CountTokens in Go, and the
equivalent idiom elsewhere. The behavior, the gate, and the numbers do not vary by language.