Core¶
Auto-generated from source docstrings (mkdocstrings). The complete public contract is agentmaker.__all__ plus each subpackage __all__.
agentmaker.core: framework foundation (LLM client, unified response types, message types, and unified exceptions), with no dependency on any agentmaker subsystem.
The abstract Agent base class lives in agentmaker.agents.base (it orchestrates hooks, guardrails, persistence, and other higher-level subsystems, so it belongs to the orchestration layer rather than the foundation layer).
AgentmakerError
¶
Bases: RuntimeError
Common base for all framework exceptions. Inherits RuntimeError to stay backward compatible (an old except RuntimeError still catches it),
while letting higher layers use except AgentmakerError to precisely catch "any exception agentmaker raised" without swallowing third-party RuntimeError.
LLMError
¶
Bases: AgentmakerError
Unified exception for LLM configuration or invocation. Both network failures and "malformed response structure" are normalized here for uniform catching by higher layers.
LLMConfigError
¶
Bases: LLMError
LLM configuration / dependency error: unknown provider / missing key / missing model / missing base_url at construction time, or the SDK not installed (ImportError). These are "developer configuration problems" where retrying is pointless: they should alert / fix the config directly, as distinct from the runtime LLMRequestError.
LLMRequestError
¶
Bases: LLMError
LLM runtime call failure (network / rate limit / auth / timeout). Carries structured attributes for precise higher-layer decisions:
provider / model: which call failed; status_code: the HTTP status code (extracted from the underlying SDK exception by duck typing, None if not obtainable); retryable: whether it is worth retrying (True for 408 / 429 / 5xx / timeout). Third parties use this for exponential backoff without parsing message text.
LLMResponseError
¶
Bases: LLMError
LLM response structure / parsing failure (empty choices / no candidates / request assembly failure / structured-output validation retries exhausted). Usually the fix is to change the prompt or retry the structured output rather than back off as for a network error, so it is kept separate from LLMRequestError.
ContextWindowExceeded
¶
Bases: LLMError
Raised when, after loss-aware reduction of the trajectory/history, the mandatory-to-keep portion (protected head + the most recent few entries) still exceeds the model's context window budget.
This is an actionable error meaning "this task really is too large for this model" (consider splitting the task / switching to a model with a larger window), and it never silently truncates away signal.
Inherits LLMError, so except LLMError also catches it.
RetrievalError
¶
Bases: AgentmakerError
Unified exception for the retrieval subsystem (embedding / vector store / retrieval). Configuration, missing dependencies, and invocation and storage failures are all normalized here.
SessionError
¶
Bases: AgentmakerError
Unified exception for session persistence (SessionStore). Failures opening / reading / writing the session store are all normalized here.
GuardrailTripwireError
¶
Bases: AgentmakerError
Raised when a guardrail trips (input / output violation); the exception's str is the readable block explanation shown to the user.
RunLimitExceeded
¶
Bases: AgentmakerError
Raised when a single run exceeds a RunPolicy limit (wall-time / number of LLM calls / number of tool calls / token cap), aborting this round.
RunCancelled
¶
Bases: AgentmakerError
Raised when a single run is aborted by RunPolicy's cooperative cancellation (the cancel callback returns True).
ToolError
¶
Bases: AgentmakerError
Unified exception for the tool subsystem (tool integration / registration / execution): missing dependencies (e.g. mcp not installed) and registration failures are all normalized here
(the same convention as RetrievalError, where "a domain exception covers that domain's missing dependencies", making except ToolError precise for tool-domain problems).
ToolRegistrationError
¶
Bases: ToolError, ValueError
Tool registration failure (invalid name / duplicate name without overwrite allowed). Also inherits ValueError, preserving the old except ValueError for backward compatibility.
LLMResponse
dataclass
¶
The unified result of a single (non-streaming) call. Every protocol adapter translates its raw response into this.
__str__()
¶
Let print(response) / f"{response}" show the reply text directly, for easier debugging.
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
The content text. |
LLMClient
¶
LLM front-end client: resolves configuration from an explicit provider and dispatches calls to the adapter for the corresponding protocol.
Usage (provider defaults to deepseek; each cloud vendor's default_model is set to its cheapest model,
so model may be omitted; local / self-hosted / proxy must pass model explicitly):
LLMClient() # equivalent to deepseek + deepseek-v4-flash
LLMClient("openai") # uses openai's default gpt-4.1-nano
LLMClient("openai", model="
# chat/stream are both async (the framework is fully async):
resp = await llm.chat([{"role": "user", "content": "Hello"}]); print(resp.content)
async for piece in llm.stream([{"role": "user", "content": "Tell a joke"}]): print(piece, end="")
# For synchronous spots use agentmaker.core.aio: run_sync(llm.chat(...)) / iter_sync(llm.stream(...))
print(llm.last_stream_stats)
last_stream_stats
property
¶
Return the stats of the most recent stream() (usage / latency / finish reason); None if no streaming call has been made.
Returns:
| Type | Description |
|---|---|
Optional[StreamStats]
|
Optional[StreamStats]: the streaming stats object or None. |
__init__(provider='deepseek', model=None, api_key=None, base_url=None, *, timeout=60.0, default_temperature=None, context_window=None, max_output_tokens=None, supports_function_calling=None, supports_vision=None, emulate_tools=False, profile=None)
¶
Take the config profile by provider, resolve model/key/base_url, validate, and instantiate the adapter by protocol.
Constructing the adapter sends no network request; the actual networking happens in chat()/stream().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
str
|
the vendor name in _PROFILES, defaults to "deepseek" (unknown raises and lists the options). |
'deepseek'
|
model
|
Optional[str]
|
the model name; if omitted, uses this provider's default_model (cloud vendors have the cheapest model filled in; local / self-hosted have no default and must pass it explicitly). |
None
|
api_key
|
Optional[str]
|
the API key; if omitted, resolved via the _resolve_key fallback chain. |
None
|
base_url
|
Optional[str]
|
the service URL; if omitted, resolved via the _resolve_base_url fallback chain (may be None for native protocols). |
None
|
timeout
|
float
|
timeout in seconds. |
60.0
|
default_temperature
|
framework-level default sampling temperature. Defaults to None = do not send
a temperature parameter (leave it to each model server's own default): uniformly we do not decide
temperature on the developer's behalf. For determinism / a custom temperature, pass
|
None
|
|
context_window
|
this model's context window (tokens), explicit value takes priority; if omitted, the profile value is trusted only when the model is this vendor's default_model, otherwise (switched model / local self-hosted) it is unknown (None): avoids the context budget being distorted by the wrong window. |
None
|
|
max_output_tokens
|
this model's single-call max output tokens, same resolution rule as context_window (explicit first, otherwise trust the profile only for default_model, None for a switched model / self-hosted); lets the window budget clamp the output reserve to the range the model can actually emit. |
None
|
|
supports_function_calling
|
whether native function calling is supported; None = resolve by model-level / provider-level default (see ProviderProfile.supports_function_calling), pass True/False explicitly to override. An Agent with tools that hits False fails loud at construction time. |
None
|
|
supports_vision
|
whether this model accepts image input (multimodal content parts, see core/multimodal.py); None = provider-level default from the profile (unknown providers stay None and are not blocked). False makes chat/stream fail loud before any network call when image parts are present. |
None
|
|
emulate_tools
|
bool
|
run tools via text emulation for models that do not support native function calling (opt-in). Enabling it wraps the underlying adapter with ToolEmulationAdapter: it writes the tool catalog into system, flattens the tool trace into text, and parses tool calls out of the model's plain-text reply: the agent loop is unchanged, upward it still looks like standard tool_calls, and supports_function_calling is auto-set to True (no longer fails loud). Do not enable it if native fc is available: text emulation is inherently less reliable and costs extra tokens. Defaults to False. |
False
|
profile
|
Optional[ProviderProfile]
|
an optional custom ProviderProfile: lets a developer add a custom provider without editing the framework source. If given, it is used and the built-in _PROFILES lookup is skipped, going through the exact same model/key/base_url/adapter resolution. provider here is just an identifier name (recommend passing provider="myvendor" alongside). Adding a model only needs model=; for an OpenAI-compatible service use "openai_compatible" + base_url. |
None
|
Example
LLMClient("deepseek").chat([{"role": "user", "content": "hi"}]).content LLMClient(provider="myvendor", profile=ProviderProfile(base_url=..., key_envs=("K",), default_model="m"), model="m")
chat(messages, *, temperature=None, max_tokens=None, output_schema=None, **kwargs)
async
¶
One-shot (non-streaming) async call, delegated to the underlying adapter, returning a unified LLMResponse (the framework is fully async: this is the only call entry point).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[dict]
|
unified message list. |
required |
temperature
|
sampling temperature, None uses the default. |
None
|
|
max_tokens
|
max generated tokens, optional. |
None
|
|
output_schema
|
optional JSON Schema (dict); if given, requires the model to output per it: the adapter translates it into response_format json_schema / json_object / Anthropic output_config / Gemini responseSchema based on provider capability. Usually passed by the upper-layer harness.structured (which then does pydantic validation + retry on failure, see 2.3). |
None
|
|
**kwargs
|
other parameters passed through to the underlying SDK. |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
LLMResponse |
LLMResponse
|
unified response object (awaiting it means "wait for this call to finish and get the result", deterministic order). |
Synchronous calls: driven via agentmaker.core.aio.run_sync(client.chat(...)) (the framework's synchronous facade routes through it uniformly).
stream(messages, *, temperature=None, max_tokens=None, on_stats=None, **kwargs)
async
¶
Streaming async call (async generator), delegated to the underlying adapter to emit text piece by piece.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[dict]
|
unified message list. |
required |
temperature
|
sampling temperature, None uses the default. |
None
|
|
max_tokens
|
max generated tokens, optional. |
None
|
|
on_stats
|
optional callback (StreamStats) -> None; hands back stats (usage / latency / finish reason) for this call when the stream is exhausted. More reliable than reading last_stream_stats: concurrent streams on a shared client overwrite last_stream_stats. |
None
|
|
**kwargs
|
other parameters passed through to the underlying SDK. |
{}
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[str]
|
The text deltas emitted piece by piece (consumed via async for; synchronous consumption via |
Note: without tools the stream is str-only (fully backward compatible). With tools passed, the stream additionally yields exactly one final LLMResponse after the text drains -- content is the joined text, tool_calls carries the accumulated calls (or None). This terminal response is the channel the Agent streaming tool loop consumes; plain-text callers never see it.
ModelInfo
dataclass
¶
Objective parameters of a specific model (window / output cap), for context engineering to compute the budget from real values.
Purpose: the window/output of default_model live in their respective ProviderProfile; non-default but
commonly used models are registered in _KNOWN_MODELS so that switch-model calls like
LLMClient(provider, model="gpt-5.4-nano") also auto-resolve their window/output (otherwise a
non-default_model window is unknown and the window budget breaks). It records the two numeric window/output
parameters plus an optional fc capability override supports_function_calling (defaults to None = follow
the provider); max_tokens_field / structured_output are still taken at the provider level (they are
basically uniform within a vendor; the only exception is moonshot old v1 = json_object while K2.x =
json_schema, and following the v1 default of json_object does not error, it just does not use K2's
strict mode). Fill values from official docs and re-check as models update.
ProviderProfile
dataclass
¶
Default configuration for a single provider.
Fields (the provider name is the _PROFILES key, not repeated here): base_url: default base URL; openai / the generic fallback set this to None and read a generic env var instead. key_envs: the API key env var names this vendor conventionally uses (in priority order). default_key: placeholder key for local services that do not validate the key. default_model: fallback model name, set to the vendor's cheapest real model; used when model= is not passed. Models change as vendors update, so verify periodically and only fill in names that actually exist. protocol: decides which adapter to use (openai / anthropic / gemini). reads_generic_base_url: whether to accept the generic OPENAI_BASE_URL / LLM_BASE_URL. max_tokens_field: the field name that caps output length (openai protocol only). Defaults to max_tokens; OpenAI reasoning models require max_completion_tokens, and kimi (moonshot) has officially deprecated max_tokens in favor of max_completion_tokens, so those two override it. deepseek / qwen (dashscope, whose official compatibility docs use max_tokens) / zhipu (whose thinking goes through a separate thinking parameter) etc. keep the default max_tokens. context_window: the context window size (tokens) of default_model, for context engineering to estimate the budget against the real window. This is objective vendor data (like base_url): verify against official docs and re-check as models update; for local / self-hosted / proxy models the model is user-chosen, so set None when the window is unknown. max_output_tokens: the "max output tokens" (generation cap) for a single call of default_model, for the window budget to estimate the output reserve. It and context_window are two decoupled quantities: vendor windows can reach 1M, yet single-call output caps are commonly only 8K to 128K and do not scale with the window, so each must be looked up from its own official value. The window budget uses this to clamp the "output reserve" to the range the model can actually emit (see WindowBudgetConfig.output_reserve), avoiding reserving a dead zone on a large window that the model can never fill. Set None when unknown for local / self-hosted / proxy. structured_output: this vendor's structured output capability (only the OpenAI protocol adapter branches on it): "json_schema" (response_format json_schema, schema carried at the API layer, e.g. real openai / gemini_openai), "json_object" (only guarantees valid JSON, schema filled in by prompt + validation, e.g. deepseek/qwen/kimi/glm), "none" (does not send response_format, pure prompt as backstop, e.g. local/proxy/unknown). The anthropic/gemini native protocols always go through their own native path (output_config / responseSchema); their value "native" is annotation only and the adapter does not read it. supports_function_calling: whether this provider supports native function calling (tool calls) by default. The framework dropped the text protocol and only uses native fc, so tool calls depend on it. The six commercial vendors + the mainstream local inference stacks (vLLM/Ollama/llama.cpp) all support it, so it defaults to True; the few models that do not (e.g. some pure reasoning models) are overridden at the model level in _KNOWN_MODELS, or by explicitly passing supports_function_calling=False when constructing LLMClient. An Agent with tools that hits False fails loud at construction time (see agents/agent.py) rather than failing silently at runtime. To make an fc-less model actually usable with tools, construct LLMClient(..., emulate_tools=True) to enable the text-emulation shim (adapters/tool_emulation.py, which flips this capability bit to True). supports_vision: whether this provider's chat models accept image input (multimodal content parts, see core/multimodal.py). True/False when the provider-wide answer is known from the official docs; None = unknown (the framework does not block, the server decides). Like other vendor facts, verify periodically; override per client via LLMClient(supports_vision=...).
Message
dataclass
¶
A single conversation message.
Attributes:
| Name | Type | Description |
|---|---|---|
content |
MessageContent
|
The message body: a plain str, or a list of multimodal content parts (text / image, see core.multimodal for the neutral part shapes). Consumers that need text must go through content_text() instead of assuming str. |
role |
MessageRole
|
The role; see MessageRole for allowed values. |
timestamp |
datetime
|
The creation time, defaulting to the current UTC time (timezone-aware). |
metadata |
Dict[str, Any]
|
Accompanying information (e.g. source, token count), an empty dict by default, for logging / future feature extension. |
__post_init__()
¶
Validate that role is legal after construction: Literal only takes effect at type-check time, and an illegal value may still be passed at runtime, so add a lightweight runtime check here.
to_dict()
¶
Convert to an OpenAI-style {"role", "content"} dict for direct consumption by adapters (multimodal part lists pass through as-is; each adapter translates them to its wire format).
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Dict[str, Any]: Contains only role and content, without timestamp / metadata. |
__str__()
¶
Let print(message) show "[role] body" directly, for easier debugging (image parts render as "[image: ...]" placeholders, see content_text).
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
Of the form "[user] hello". |
content_text(content)
¶
Flatten content into plain text: str passes through; part lists join text parts with newlines and render each image as an "[image: ...]" placeholder (so summaries / keyword indexes / logs stay meaningful instead of showing a Python repr).
content_tokens(content, counter=count_tokens)
¶
Estimate the token count of content: text via the pluggable counter, plus a flat IMAGE_TOKEN_ESTIMATE per image part (see the constant's comment). None counts as 0.
image_part_from_bytes(data, media_type)
¶
Build an inline image part from raw bytes (base64-encoded for transport).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
Raw image bytes. |
required |
media_type
|
str
|
One of IMAGE_MEDIA_TYPES, e.g. "image/png". |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
Unsupported media type (fail at construction, not server-side). |
image_part_from_file(path, media_type=None)
¶
Build an inline image part from a local file (media type inferred from the suffix when omitted).
Raises:
| Type | Description |
|---|---|
ValueError
|
Suffix not recognized and media_type not given, or unsupported media type. |
image_part_from_url(url)
¶
Build a remote-image part (the provider fetches the URL; Gemini's adapter does not support this, see its docs).
messages_have_images(messages)
¶
True when any message in the list carries image parts (accepts dicts or Message-likes).
text_part(text)
¶
Build a text content part.
count_tokens(text)
¶
Estimate the token count of text: CJK plus Japanese/Korean characters count as 1 token each, everything else at roughly 4 characters per token.
This is a pre-send context budget estimate and does not feed billing or quota (cost and limits always use the real usage returned by the LLM). It does not tokenize on whitespace, so long whitespace-free runs (base64 / compressed data / very long URLs) are correctly covered by "other characters / 4" rather than being counted as a single token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The text to measure. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The estimated token count. |