Skip to content

Pinned semantics

The framework has two implementations of the same API — TypeScript (framework-agents on npm, packages/typescript) and Python (framework-agents on PyPI, packages/python). TypeScript is the reference implementation; the Python port targets API parity with the fixed TS semantics.

Most of the rules below were not design preferences. They are regression pins: each one encodes the behavior the TypeScript line after a real bug (or a wire-contract ambiguity) proved which behavior was correct. They are recorded so that both implementations — and anything that speaks to either over the wire — agree forever. The table is normative in both languages; the sections after it explain where each rule came from.

Pinned semantic Why
reasoning_tokens is a SUBSET of output_tokens never double-billed in budgets/cost
cost_usd only when a price is known unknown models → None, never a fake $0
save() APPENDS; the loop does clear-then-save history never duplicates across runs
Structured 'auto' resolves by capability unresolved auto never throws STRUCTURED_NO_MATCH
finish_reason=length + zero content → loud MODEL_ERROR a thinking model burning its whole budget is not a silent empty answer
Tool parameter schemas ride the wire models see real argument shapes
HITL decisions: exact key THEN bare tool name resume converges even when models regenerate arg text
Local ollama: pricing wins over name collisions never bill cloud prices for free local models
Storage keys/threadIds reject traversal (..) no writes outside the root
Remote SPARQL: descriptive UA + 429/5xx retry honoring Retry-After public endpoints reject and rate-limit otherwise

Naming note: TypeScript spells these fields reasoningTokens, outputTokens, costUsd, finishReason; Python spells them reasoning_tokens, output_tokens, cost_usd, finish_reason. The rules are identical; only the casing differs.

Why each pin exists

1. Reasoning tokens are a subset of output tokens

Providers report reasoning_tokens (thinking budget spend) inside the output_tokens total, not on top of it. Budget checks and cost math used to add the two fields together, which double-billed every thinking model — a run could blow the output-token budget (or overstate cost) by counting the same tokens twice. Now TokenUsage.reasoningTokens / reasoning_tokens is a reporting-only field: budgets and cost always operate on output_tokens. Both implementations compute budgets on cumulative usage, checked after every model call.

2. Cost only when a price is known

Everyone's provider sends token counts; nobody sends prices. Cost is computed client-side against a local price table, and an unknown model used to fall back to $0 — a number that looks authoritative and is silently wrong. usage.costUsd / cost_usd is set only when a price for the model exists; for unknown models it is None/undefined, never a fake zero. Check it against your provider bill; the table is a ladder, not ground truth.

3. save() appends; the loop does clear-then-save

The Memory interface appends on save(). The agent loop therefore must clear() before its end-of-run save() — an early version saved without clearing, so every run duplicated the previous transcript and history grew quadratically. The split is pinned: save() stays append (so callers can use Memory directly), and the loop's clear-then-save sequence is part of the contract, not an implementation detail.

4. Structured 'auto' resolves by capability

responseFormat: 'auto' means "pick a strategy for me". It resolves by provider capability against the model's capabilities: provider when the model advertises structured_output, the tool-based strategy otherwise. An unresolved 'auto' used to fall through as "no strategy", and every run threw STRUCTURED_NO_MATCH. The resolution is deterministic per model, so the failure mode is gone entirely — 'auto' always has a strategy.

5. Truncation with zero content is a loud MODEL_ERROR

A thinking model that spends its entire max_tokens on an unfinished reasoning trace used to come back as (what looked like) a successful, empty answer — the worst failure mode: silent and cheap-looking. Now finish_reason == "length" combined with zero content and no tool calls raises a loud MODEL_ERROR with the fix spelled out (raise max_tokens); partial content keeps flowing and merely carries the finish_reason attached.

6. Tool parameter schemas ride the wire

Tool definitions were sent with only names and descriptions, so models guessed argument shapes and emitted structurally invalid calls. The JSON Schema derived from each tool's validation schema (zod in TS, pydantic in Python — function.parameters on OpenAI-compatible wire forms) is sent with every request. Models see the real argument shapes.

7. HITL decisions: exact key, then bare tool name

Human-in-the-loop stores approval decisions in the invocation state under hitl_key(tool, args) — a key derived from the tool name plus its arguments. On resume, a model frequently regenerates the argument text (spacing, ordering, quoting), so the exact key never matches and every resumed run interrupted again — HITL could never converge. Resume now looks up the exact hitl_key(tool, args) first, then falls back to the bare tool name as a wildcard. The fallback is deliberate: a decision you already gave for a tool applies to the tool.

8. Local ollama: pricing wins over name collisions

qwen2.5 served by a local Ollama instance has the same name as a cloud-hosted model with a real price per token. Name-matching used to pick the cloud row, so free local inference was billed at cloud rates. Price lookup checks the local/free table first; local ollama: models resolve to free before cloud-name collisions are consulted.

9. Storage keys and threadIds reject path traversal

Session storage keys and checkpoint threadIds become filesystem paths. A key like "../.env" (or a threadId containing /) used to escape the storage root and write (or read) outside it. Keys containing .., and threadIds that are not a single clean directory name ("", ., .., or anything with /), now raise a non-retryable STORAGE_KEY_INVALID / THREAD_ID_INVALID before anything touches disk.

10. Remote SPARQL: descriptive UA and Retry-After-aware retries

A remote SPARQL adapter is only useful if real public endpoints (Wikidata's query service notably) actually answer it — and they reject requests without a descriptive User-Agent with 403, then rate-limit aggressively (frequent 429 responses, often with empty bodies). The adapter ships a descriptive UA by default and retries 429/5xx/network errors, honoring the Retry-After header (exponential backoff, capped) when present.

Where the bodies are buried

Each pin is enforced by tests in both trees: the TypeScript regression suites in packages/typescript/tests/ and the Python offline parity suite in packages/python/tests/ (run together with the cross-language conformance suites; see Cross-language parity). If you think a pin is wrong, change it in both languages in the same commit — or the cross-language wire tests will find you.