Releases: emberio-labs/ember
Release list
v0.6.0 — Streaming tool calls
ember 0.6.0
Tool calling now works in streaming mode, and agent memory got a strict, predictable session contract.
Highlights
Tool calling in streaming mode (#45)
Agent.stream_run() no longer refuses to run with tools — it executes tool calls in a loop exactly like run(): results go back to the model as tool messages, and generation continues until the final text answer.
for fragment in agent.stream_run("What's the weather in Moscow?"):
print(fragment, end="", flush=True) • StreamChunk gained a tool_calls field. The adapter emits it exactly once per round — in the final chunk, fully reassembled from the stream deltas, with delta empty; it is None in every other chunk.
• OpenAIProvider.stream() reassembles tool-call deltas, including parallel calls, which the OpenAI SDK splits across chunks by index and by field (id / name / arguments).
• ToolCallLimitError semantics now match run(): the shared _tool_call_limit_error() helper guards both paths, so max_tool_steps (default 10) applies identically.
Two things worth knowing:
• The stream is not segmented into rounds: interim replies before tool calls and the final answer arrive back-to-back in a single stream, and you cannot tell them apart. Use the non-streaming run() if you need that.
• While a tool runs (a shell command, an MCP server call) the stream is simply silent — Iterator[str] has no way to signal "work in progress".
Stricter session IDs in memory (#46)
Session IDs used to be silently sanitized: every character outside [A-Za-z0-9_.-] was replaced with _. That was not reversible — "ручная" and "ручной" both collapsed into the file ______.json, the second save_session() overwrote the first, and load_session() returned someone else's data with no indication of error. The same collision broke exclude_session_id in search(): the current session was not excluded from recall.
IDs are now validated instead of rewritten. Allowed: Latin letters, digits, _, -, and . (not as the first or last character), up to 128 characters. Anything else raises InvalidSessionIdError — a subclass of ValueError, so existing except ValueError handlers keep working.
The ASCII restriction is deliberate: on APFS/HFS+ filenames are normalized, and Unicode IDs would reintroduce unresolvable collisions. Since a valid ID equals the filename without its extension, the ID is recovered unambiguously from the directory.
▌ Migration: if you pass non-ASCII or otherwise unusual IDs today, they were
▌ silently mangled before. Rename them to valid IDs — or, if you rely on the
▌ old behavior, sanitize them yourself before constructing the agent.
Enumerate sessions: Memory.list_sessions() (#46)
Consumers (CLIs, UIs) no longer need to read *.json files directly just to show a session list.
from ember import FileMemory
memory = FileMemory("/tmp/ember-memory")
for info in memory.list_sessions():
print(info.session_id, info.message_count, info.updated_at)
# user-42 2 2026-09-11 04:25:58+00:00 • New SessionInfo dataclass (frozen, slots): session_id, message_count, updated_at (UTC), and size_bytes (None when the backend doesn't track size).
• list_sessions() is an abstractmethod like the rest of Memory — custom backends (Redis, Postgres, SQLite) must implement it.
• Ordering is part of the contract: newest first, ties broken by ascending session_id, so the output can be rendered as-is.
• FileMemory counts non-empty JSONL lines without parsing content (one corrupt file doesn't break listing the others) and filters stray *.json files by the same rule as search().
Installation
pip install "emberio-labs-ember[openai]" # with OpenAI support
pip install "emberio-labs-ember[mcp]" # with MCP client support
pip install emberio-labs-ember # core (no providers) On PyPI the package is published as emberio-labs-ember; the import name is still
ember.
Compatibility
No public API removals. Behavior changes to be aware of:
• stream_run() with tools set used to raise ValueError; it now runs the tool loop.
• Invalid session_id values that used to be silently sanitized now raise InvalidSessionIdError (a ValueError subclass).
v0.5.0 — Agent Skills
ember v0.5.0 — Agent Skills
ember 0.5.0 adds support for Agent Skills — a portable, open format for packaging instructions an agent can discover and load on demand. Skills follow the progressive disclosure principle: the model always sees only a tier-1 catalogue (name + description), and pulls in the full instruction body with a tool call when it becomes relevant. Because the format is a standard, skills are portable across compatible clients.
Highlights
- 📚 Agent Skills — load skills from one or more directories, no extra glue code.
- 🔍 Progressive disclosure — compact catalogue in the prompt, full body only when needed.
- ✍️
read_skill/save_skilltools — the model can load and persist skills during a run. - 🔒 Strict on write, lenient on read — authored skills are validated against the
standard; third-party skills that don't parse perfectly are skipped with a warning. - 🧩 Composes with everything else — skills coexist with
FunctionTools and MCP tools.
New public API
| Name | Description |
|---|---|
Agent(skills_dirs=[...]) |
One or more skill directories; order defines priority |
ember.Skill |
Skill data model (name, description, optional standard fields) |
ember.skills.parse_skill |
Lenient parser for a single SKILL.md |
ember.skills.scan_skills |
Scan directories for <name>/SKILL.md |
ember.skills.SkillStore |
Catalogue for the prompt + the read_skill / save_skill tools |
Usage
pip install -U emberio-labs-ember from ember import Agent, MockProvider
agent = Agent(
provider=MockProvider(),
skills_dirs=[".ember/skills", "~/.config/ember/skills"], # order = priority
system_prompt="You are a developer assistant.",
) A skill is a directory <name>/SKILL.md with YAML frontmatter (name, description) and a Markdown body:
---
name: release-checklist
description: Steps to follow before publishing an ember release.
---
1. Bump the version in three places... Behaviour notes
- Directories are scanned at
<dir>/<name>/SKILL.md(one level deep only); on a name collision the skill from the earlier directory wins. - The catalogue is re-read from disk on every request, so a skill saved during the current conversation is visible to the very next model call. The agent's message history is not modified by the catalogue.
save_skillwrites only on an explicit user request and only into the first directory in the list. The tool validates the standard strictly — name (lowercase Latin letters, digits and single hyphens, ≤64 chars), description (≤1024 chars), non-empty body — and refuses to write outside its target directory.skills_dirs=None(the default) disables skills entirely: no tools, no prompt section. The library ships no default paths by design.- Optional standard fields (
license,compatibility,metadata,allowed-tools) are parsed and preserved, but are not executed in v1.
Limitations
stream_run()does not support skills or tools; userun().pyyamlis now a core dependency (no extra required).⚠️ Skills from untrusted sources can inject instructions into the model's context. Only enable directories you trust. A trust gate is planned.
What's Changed
Tests
146 tests passing on Python 3.10–3.12; ruff and mypy (strict) clean.
Full Changelog:
v0.4.0...v0.5.0
v0.4.0 — Persistent agent memory
v0.4.0 — Persistent agent memory
What's new
Persistent agent memory (#24) — the agent now survives restarts and recalls context from past sessions. Conversations can be stored between runs, an agent can be recreated on top of its own history, and relevant fragments of older dialogs are mixed into new ones automatically.
from pathlib import Path
from ember import Agent, FileMemory, OpenAIProvider
memory = FileMemory(Path(".ember/memory"))
agent = Agent(
provider=OpenAIProvider(api_key=os.environ["OPENAI_API_KEY"]),
memory=memory,
session_id="daily-standup",
)
print(agent.run("What did we decide yesterday?")) • New ember.memory subpackage: Memory — abstract storage interface (load_session / save_session / search / delete_session), so you can plug in your own backend (SQLite, Redis, Postgres…). FileMemory(directory) — file-based storage: one JSONL file per session, atomic writes (tmp + rename), sanitized session_id so history can't escape the directory.
• Agent accepts memory + session_id (both or neither, otherwise ValueError): on creation it loads the saved history of the session and keeps working — the agent outlives process restarts.
• Recall from past sessions: each run searches older sessions (bag-of-words) and injects relevant fragments as a separate system message — the conversation history and the store stay untouched. • The conversation is saved after every run() / stream_run(), including exceptions and stream interruptions (try/finally); the system prompt is never stored.
• reset() starts over and rewrites the current session to empty.
• Fully backwards compatible: without memory the agent works exactly as before.
Installation
pip install "emberio-labs-ember" # agent memory is in the core, no extra
needed
pip install "emberio-labs-ember[openai]" # OpenAI-compatible APIs
pip install "emberio-labs-ember[mcp]" # MCP client
Full Changelog: v0.3.0...v0.4.0 PyPI:
https://pypi.org/project/emberio-labs-ember/0.4.0/
v0.3.0 — OpenAI-compatible APIs & MCP HTTP headers
What's new
OpenAI-compatible APIs (#20, #23) — OpenAIProvider now accepts
base_url, unlocking every provider that speaks the Chat Completions
protocol with a single adapter: cloud (OpenRouter, Groq, DeepSeek, Mistral,
Perplexity, xAI, Together, Fireworks) and local/self-hosted servers
(LM Studio, vLLM, LocalAI, Ollama).
from ember import Agent, OpenAIProvider
# cloud provider (e.g. Groq)
agent = Agent(
provider=OpenAIProvider(
api_key=os.environ["GROQ_API_KEY"],
base_url="https://api.groq.com/openai/v1",
model="llama-3.1-8b-instant",
),
)
# local server (e.g. LM Studio) — pass a dummy key
provider = OpenAIProvider(
api_key="sk-local",
base_url="http://localhost:1234/v1",
model="local-model",
)
• New constructor parameter base_url: str | None = None — None keeps the
default OpenAI endpoint, so existing code is unaffected.
• base_url is the full API URL: the SDK appends /chat/completions itself,
nobody adds /v1 for you.
• api_key stays required; config is passed explicitly (nothing is read from env
/ .env).
• Works with complete(), stream() and tool calling; also available via
get_provider("openai", api_key=..., base_url=...).
HTTP headers in MCPClient.http (#21, #22) — MCPClient.http(...) now accepts
headers={...}, so you can connect to MCP servers protected by authentication
(Bearer tokens, API keys, custom headers).
from ember import MCPClient
with MCPClient.http(
"https://mcp.example.com/mcp",
headers={"Authorization": "Bearer YOUR_TOKEN"},
) as mcp:
tools = mcp.list_tools() • Headers are sent with every request of the session (session init, tools/list,
tools/call), not just the first one.
• stdio transport is unaffected (headers are ignored there).
• Fully backwards compatible: MCPClient.http(url) without headers works as
before.
Installation
pip install "emberio-labs-ember[openai]" # OpenAI-compatible APIs
pip install "emberio-labs-ember[mcp]" # MCP client
Full Changelog: v0.2.0...v0.3.0
PyPI: https://pypi.org/project/emberio-labs-ember/0.3.0/
v0.2.0 — MCP client
What's new
MCP client (#14, #19) — integration with external Model Context
Protocol servers. The ember agent can now
execute tools from any MCP server alongside local functions — the server stays
a separate process and doesn't know about the client.
New ember.mcp subpackage:
MCPClient— synchronous client for a single MCP server: a thin wrapper
around the async MCP SDK (background thread with an event loop, blocking
methods).- Two connection factories:
MCPClient.stdio(command, args, ...)— server as a child process (stdio
transport);MCPClient.http(url, ...)— server over streamable HTTP.
- Context manager:
with MCPClient.stdio(...) as mcp:— connect/disconnect
handled automatically. mcp.list_tools()— returns the server's tools (tools/list) as
regularFunctionTools: the JSON Schema is preserved, andfuncproxies
calls back to the server. The list can be passed toAgentalongside local
tools.mcp.call_tool(name, arguments)— direct tool invocation
(tools/call).MCPError— a single error type for transport and protocol failures
(modeled afterProviderError), no dependency on MCP SDK internals.
Example
from ember import Agent, MCPClient
with MCPClient.stdio(command="python", args=["server.py"]) as mcp:
agent = Agent(provider=provider, tools=mcp.list_tools())
print(agent.run("Call the ping tool")) A runnable example without API keys: examples/mcp_client.py (includes a --serve
mode that starts a local stdio server with a ping tool).
Installation
pip install "emberio-labs-ember[mcp]"
Full Changelog: v0.1.0...v0.2.0
PyPI: https://pypi.org/project/emberio-labs-ember/0.2.0/