Getting started: TypeScript¶
Install¶
Requirements: Node.js >= 20. The framework has three runtime dependencies only (dotenv, zod, zod-to-json-schema) and talks to providers with plain fetch — no provider SDKs.
The framework needs a model endpoint. initChatModel resolves a provider:model string against environment config:
together:<id>or a bare<id>→ TogetherAI (OpenAI-compatible)openai:<id>→ OpenAIanthropic:<id>→ Anthropic native Messages APIollama:<id>→ local Ollama (no key)custom:<id>→ any OpenAI-compatible endpoint (baseUrl+apiKeyrequired)
.env keys¶
Config loading is silent and never throws: .env is read from cwd, then up to three parent directories walk upward. Set FRAMEWORK_ENV_PATH to point elsewhere. When TOGETHER_API_KEY is set, TogetherAI wins for loadConfig().active; otherwise OpenAI defaults apply.
| Key | Default | Purpose |
|---|---|---|
TOGETHER_API_KEY |
empty | TogetherAI key |
TOGETHER_BASE_URL |
https://api.together.xyz/v1 |
TogetherAI endpoint |
TOGETHER_MODEL |
Qwen/Qwen3.5-9B |
Default chat model |
OPENAI_API_KEY |
empty | OpenAI key |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
OpenAI endpoint |
OPENAI_MODEL |
gpt-4o-mini |
Default OpenAI model |
ANTHROPIC_API_KEY |
empty | Anthropic key (native API) |
ANTHROPIC_BASE_URL |
https://api.anthropic.com |
Anthropic endpoint |
ANTHROPIC_MODEL |
claude-sonnet-4-6 |
Default Anthropic model |
OLLAMA_BASE_URL |
http://localhost:11434/v1 |
Local Ollama endpoint |
OLLAMA_MODEL |
qwen2.5 |
Default Ollama model |
First agent + first tool¶
defineTool takes { name, description, schema, execute }. The zod schema is the contract: it is converted to a JSON Schema and sent to the provider, so the model sees the real argument shape (without parameters, OpenAI-compatible APIs advertise a zero-arg tool).
import { Agent, defineTool, initChatModel } from "framework-agents";
import { z } from "zod";
const getWeather = defineTool({
name: "get_weather",
description: "Current weather for a city",
schema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, tempC: 21 }),
});
const agent = new Agent({
name: "assistant",
model: initChatModel("together:Qwen/Qwen3.5-9B", { stream: true }),
tools: [getWeather],
systemPrompt: "You are a concise weather assistant.",
memory: undefined, // omit for single-shot agents
maxSteps: 6, // default 6
});
const result = await agent.run("Weather in Tokyo?");
console.log(result.text, result.toolCalls, result.usage);
The second execute parameter is an optional ToolContext ({ agentName, state, invocationState }) — see agents & tools.
Streaming¶
run() drains stream() internally; both fire events hooks identically. Consume events directly with for await:
for await (const event of agent.stream("Weather in Tokyo?")) {
switch (event.type) {
case "thinking": process.stdout.write(event.delta); break; // reasoning deltas
case "token": process.stdout.write(event.delta); break;
case "tool_call": console.log("calling", event.name); break;
case "tool_result": console.log(event.name, "→", event.output); break;
case "done": console.log("\nfinal:", event.text); break;
}
}
Events are step | token | thinking | tool_call | tool_result | done. Token and thinking deltas are batched per model call; tool events yield live. Details in streaming.
Budgets¶
Caps are enforced after every model call against cumulative usage:
import { Agent, initChatModel, TokenBudgets } from "framework-agents";
const agent = new Agent({
model: initChatModel("together:Qwen/Qwen3.5-9B"),
budgets: {
maxTokens: 8000, // input + output per run (reasoning is a subset, never added)
maxCostUsd: 0.05, // needs a known per-model price or budgets.prices
deadlineMs: 60_000,
} satisfies TokenBudgets,
maxToolCalls: 10, // separate option; exceeded → BUDGET_EXCEEDED
});
Exceeding any cap throws AgentError("BUDGET_EXCEEDED") (non-retryable). See budgets & pricing.
Run the offline examples¶
The examples/ directory in the package runs on deterministic mock providers — no API key, no cost:
Live examples (examples/live/) hit the real TogetherAI API and need TOGETHER_API_KEY — see live examples.