AI Agent Platform - Core SDK (Python)
A truly functional AI agent platform core SDK for building conversational applications with any LLM provider through an extensible plugin system.
Functional Architecture
This SDK follows pure functional programming principles:
- Immutable Data Structures - All data types are frozen dataclasses
- Pure Functions - AgentCore methods take session/config and return new objects
- Stateless Core - Core stores only the processing pipeline (plugins), not runtime state
- Application-Managed State - Applications manage sessions and configuration
- No Side Effects - All transformations are explicit and traceable
Features
- Provider Agnostic: Switch between OpenAI, Anthropic, Google, local models
- Extensible: Add features without touching core code
- Functional Design: Predictable, testable, composable
- Streaming-First: Real-time response display
- Immutable: All state changes return new objects
Installation
These commands install only the agent_core package from core/python. They
do not install application/python or any repo-root plugin packages under
plugins/.
From Source
cd core/python
python -m pip install -e .
Development Installation
cd core/python
python -m pip install -e ".[dev]"
Quick Start
from agent_core import AgentCore
from plugins.openai_provider import OpenAICompatibleProvider
# Core stores only processing pipeline (stateless)
core = AgentCore()
core.register_provider(OpenAICompatibleProvider)
# Application manages config
config = {
"provider": "openai_compatible",
"model": "gpt-4o",
"api_key": "sk-...",
}
# Create session (immutable data structure)
session = core.create_session()
# Add messages (functional - returns new session)
session = core.add_message(session, "system", "You are a helpful assistant.")
session = core.add_message(session, "user", "What is 2+2?")
# Send request (functional - returns new session + final messages list)
session, messages = core.send_request(session, config)
print(messages[-1]["content"]) # "4"
# Original operations don't modify - they return new objects
print(len(session.messages)) # 3 (system + user + assistant)
Streaming Example
from agent_core import AgentCore
from plugins.openai_provider import OpenAICompatibleProvider
core = AgentCore()
core.register_provider(OpenAICompatibleProvider)
config = {
"provider": "openai_compatible",
"model": "gpt-4o",
"api_key": "sk-...",
}
session = core.create_session()
session = core.add_message(session, "user", "Write a short poem")
# Stream response (generator yields partials, then final session)
for chunk in core.send_request_stream(session, config):
if chunk["type"] == "partial":
print(chunk["message"]["content"], end="", flush=True)
elif chunk["type"] == "final":
# Get new session with response
session = chunk["session"]
print("\n[Done]")
Core Concepts
Provider Selection
When you register multiple providers with AgentCore, each request selects a provider via the provider key in the config dict passed to send_request / send_request_stream / send_request_stream_async.
- When exactly one provider is registered,
provideris optional (the single provider is used by default), but it is recommended to set it for clarity. - When multiple providers are registered,
provideris required and may be either the provider's configured name (for example"openai_compatible") or a fully-qualified class path such as"plugins.openai_provider.OpenAICompatibleProvider".
Immutable Session
Sessions are frozen dataclasses - pure data containers with no methods:
from agent_core import Session, Message
# Create session
session = Session(
session_id="my-session",
messages=[
Message(role="user", content="Hello"),
Message(role="assistant", content="Hi there!")
]
)
# Cannot modify - it's frozen
# Avoid mutating session.messages directly; create a new Session instead
# session.session_id = "new" # Error! Dataclass is frozen
Functional Transformations
All transformations are pure functions that return new objects:
# Add message (returns NEW session)
new_session = core.add_message(session, "user", "Hello")
# Original unchanged
assert len(session.messages) == 0
assert len(new_session.messages) == 1
# Send request (returns NEW session + response)
final_session, response = core.send_request(new_session, config)
assert len(new_session.messages) == 1 # Still 1
assert len(final_session.messages) == 2 # Has response
Stateless Core
Core stores only the plugin pipeline, not runtime state:
core = AgentCore()
# Register plugins (just classes, no config)
core.register_provider(OpenAICompatibleProvider)
core.register_feature(WebSearchFeature)
core.register_tool(CalculatorTool)
# Core doesn't store sessions or config
# Application manages them
session1 = core.create_session("user-1")
session2 = core.create_session("user-2")
config1 = {"provider": "openai_compatible", "model": "gpt-4"}
config2 = {"provider": "openai_compatible", "model": "gpt-3.5-turbo"}
# Same core, different sessions/configs
session1, resp1 = core.send_request(session1, config1)
session2, resp2 = core.send_request(session2, config2)
Plugin Classes
Plugins are plain classes with instance methods (functional style; pass state explicitly). Note: keep methods stateless and treat any object attributes as per‑request only; instances are short‑lived and should not retain durable state. Example provider:
from typing import Dict, Any, List, Tuple
class MyProvider:
name: str = "my_provider"
version: str = "1.0.0"
def get_config_schema(self) -> Dict[str, Any]:
return {"api_key": {"type": "string", "required": True}}
def init(self, config: Dict[str, Any]) -> Dict[str, Any]:
# Provider state is an opaque dict; this minimal example keeps
# configuration under a single "config" key and is free to add
# any other per-request data it needs.
return {"config": config}
def call_api(
self,
native_messages: List[Dict[str, Any]],
state: Dict[str, Any],
) -> Tuple[
List[Dict[str, Any]],
List[Dict[str, Any]],
List[Dict[str, Any]],
Dict[str, Any],
]:
# Return (partial_messages, final_messages, native_messages, new_state)
msg = {"role": "assistant", "content": "Response", "metadata": {}}
return ([], [msg], [*native_messages, msg], state)
In the Python SDK, adapter classes (for example ProviderDefaultsAdapter) supply default implementations for hooks like to_native_messages, from_native_messages, initialize_request, finalize, and stream_api when they are omitted, so most providers only need to implement configuration, init, and one of the I/O entry points (call_api or stream_api).
Running Tests
cd core/python
pytest
By default pytest is configured with -m 'not integration', so tests marked as integration are skipped unless explicitly selected. Useful commands:
pytest tests -m integration -q– run all integration testspytest tests -m ollama -q– run only Ollama tests
OpenRouter integration tests live under plugins/openrouter/tests (repo root) and are marked with openrouter.
Run with Coverage
pytest --cov=agent_core --cov-report=html
API Reference
AgentCore
Stateless processing pipeline.
Methods
register_provider(plugin_class)- Register provider plugin classregister_feature(plugin_class)- Register feature plugin classregister_tool(plugin_class)- Register tool plugin classcreate_session(session_id=None)- Create new empty sessionadd_message(session, role, content, metadata=None, config=None)- Add message (returns new session)modify_message(session, index, content, config=None)- Modify existing message content (returns new session)send_request(session, config)- Send request (returns new session + response)send_request_stream(session, config)- Stream request (yields partials, then final session)send_request_stream_async(session, config)- Async streamingget_ui_schema(config)- Get UI schema from pluginsget_completions(config, text)- Get feature-driven completion suggestionsapply_feature_completion(config, text, completion)- Let features turn an accepted completion into a snippetget_tool_schemas(config)- Get tool schemasexport_session(session, format="json")- Export session to stringimport_session(data, format="json")- Import session from string
Session (Dataclass)
Immutable session data structure.
Attributes
session_id: str- Session identifiermessages: List[Message]- Messages (treat as immutable; use pure updates)metadata: Dict[str, Any]- Session metadata
Methods
to_dict()- Convert to dictionaryfrom_dict(data)- Create from dictionary (classmethod)
Message (Dataclass)
Immutable message structure.
Attributes
role: MessageRole- "system" | "user" | "assistant" | "tool"content: str- Message contentmetadata: Optional[Dict[str, Any]]- Optional metadata
Methods
to_dict()- Convert to dictionaryfrom_dict(data)- Create from dictionary (classmethod)
Architecture Principles
1. Immutability
All data structures are frozen dataclasses. Transformations return new instances:
# Immutable message
msg = Message(role="user", content="Test")
# msg.role = "assistant" # Error!
# Immutable session
session = Session(session_id="test", messages=[])
# session.messages.append(...) # Error!
# Functional updates
new_session = Session(
session_id=session.session_id,
messages=[*session.messages, msg]
)
2. Pure Functions
Core methods are pure (same input → same output):
session = core.create_session()
session = core.add_message(session, "user", "Hello")
# Same inputs produce same results
result1 = core.send_request(session, config)
result2 = core.send_request(session, config)
assert result1[1]["content"] == result2[1]["content"]
3. Explicit State
No hidden state - everything is passed explicitly:
# Config passed explicitly (not stored in core)
session, response = core.send_request(session, config)
# Session passed explicitly (not stored in core)
new_session = core.add_message(session, "user", "Test")
4. Application Responsibility
Applications manage state (sessions, config):
# Application manages sessions
sessions = {
"user-1": core.create_session("user-1"),
"user-2": core.create_session("user-2")
}
# Application manages config
configs = {
"fast": {"model": "gpt-3.5-turbo"},
"smart": {"model": "gpt-4"}
}
# Application decides which to use
session, response = core.send_request(
sessions["user-1"],
configs["smart"]
)
Development
Type Checking
mypy agent_core
Code Formatting
black agent_core tests
Linting
ruff check agent_core tests
Debug Logging
The SDK includes structured logging using structlog with powerful filtering capabilities via environment variables.
Quick Start
# Enable debug logging
LOG_LEVEL=DEBUG pytest tests/
# Filter logs by logger name (core, provider, etc.)
LOG_LEVEL=DEBUG LOG_SCOPE=provider pytest tests/
# Show only specific data fields
LOG_LEVEL=DEBUG LOG_DATA="chunk,partials" pytest tests/
# Hide all extra fields (show only standard fields)
LOG_LEVEL=DEBUG LOG_DATA="" pytest tests/
# Custom filtering with JSONPath support
LOG_LEVEL=DEBUG LOG_FILTER="messages[0].role:equals:system" pytest tests/
# Use in your application
LOG_LEVEL=DEBUG python your_app.py
Structlog note: The SDK uses structlog's bound logger API, where the first positional argument to methods like logger.debug() is already the event field. Do not also pass an event=... keyword argument (for example, logger.debug("message", event="partial")), as this will raise a TypeError about multiple values for event. Use a different key such as event_type or kind instead.
Environment Variables
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL |
Log level: DEBUG, INFO, WARNING, ERROR |
INFO |
LOG_SCOPE |
Filter by logger name (e.g., core, provider) |
All |
LOG_DATA |
Comma-separated whitelist of fields to show | All fields |
LOG_FILTER |
Filter expressions (comma-separated, AND logic) | None |
LOG_COLORS |
Enable/disable ANSI colors in structlog output | Enabled |
LOG_DATA Field Whitelist
Controls which data fields are displayed:
# Not set - show ALL fields (default)
LOG_LEVEL=DEBUG pytest tests/
# Empty string - show ONLY standard fields (event, timestamp, level, logger)
LOG_DATA="" LOG_LEVEL=DEBUG pytest tests/
# Whitelist - show ONLY specified fields + standard fields
LOG_DATA="chunk,partials" LOG_LEVEL=DEBUG pytest tests/
LOG_DATA="messages,native_messages" LOG_LEVEL=DEBUG pytest tests/
Available data fields:
- chunk - Raw streaming chunk from provider
- partials - Partial messages extracted from chunk
- finals - Final messages from provider
- core_partials - Partials converted to core format
- messages - Core message list
- native_messages - Provider-native message list
- final_native - Final provider-native messages
- final_core - Final core messages
LOG_FILTER Custom Expressions
Condition Types:
- field:exists - Field must exist
- field:nonempty - Field must be non-empty (strings, lists, dicts)
- field:equals:value - Field must equal value
- field:contains:substring - Field must contain substring
Path Types:
- Simple: chunk.reasoning
- Array index: messages[0].content (JSONPath)
- Nested: chunk.choices[0].delta.role
Examples:
# Simple field check
LOG_FILTER="chunk:exists"
# Nested field
LOG_FILTER="chunk.reasoning:nonempty"
# Array indexing with JSONPath
LOG_FILTER="messages[0].role:equals:system"
LOG_FILTER="messages[1].content:contains:file"
# Multiple conditions (AND logic)
LOG_FILTER="chunk:exists,partials:nonempty"
LOG_FILTER="messages[0].role:equals:system,messages[1].content:exists"
Logger Scopes
Available logger names for LOG_SCOPE:
- core - Core SDK streaming and request processing
- provider - Provider wrapper chunk processing
- extension - Extension-specific operations
- feature - Feature plugin operations
Example Output
Compact (default):
2025-12-01 14:56:36 [debug ] starting stream request [core] extensions=[] features=[] provider=dummy_provider
2025-12-01 14:56:36 [debug ] processing chunk [provider] chunk={'content': 'E'} provider=dummy_provider
With LOG_DATA whitelist:
LOG_DATA="chunk,partials" LOG_LEVEL=DEBUG pytest tests/
2025-12-01 14:56:52 [debug ] processing chunk [provider] chunk={'content': 'E'}
2025-12-01 14:56:52 [debug ] provider processed chunk [provider] partials=[{'role': 'assistant', 'content': 'E'}]
Clean output (no extra fields):
LOG_DATA="" LOG_LEVEL=DEBUG pytest tests/
2025-12-01 14:56:44 [debug ] starting stream request [core]
2025-12-01 14:56:44 [debug ] processing chunk [provider]
Advanced Examples
Debug specific message content:
# Show logs where first message is from system
LOG_FILTER="messages[0].role:equals:system" \
LOG_DATA="messages" \
LOG_LEVEL=DEBUG \
pytest tests/
Debug reasoning in chunks:
# Only show chunks with non-empty reasoning
LOG_FILTER="chunk.reasoning:nonempty" \
LOG_DATA="chunk" \
LOG_LEVEL=DEBUG \
pytest tests/
Track tool calls:
# Show logs with tool calls in messages
LOG_FILTER="messages[2].metadata:exists" \
LOG_DATA="messages" \
LOG_LEVEL=DEBUG \
pytest tests/
Combine multiple filters:
# Provider scope + specific message + show only relevant fields
LOG_SCOPE=provider \
LOG_FILTER="chunk:exists,partials:nonempty" \
LOG_DATA="chunk,partials,provider" \
LOG_LEVEL=DEBUG \
pytest tests/
Functional vs Object-Oriented
Before (OOP - Old)
core.register_provider(plugin, config={...})
session = core.create_session() # Core stores session
session.add_message("user", "Hello") # Mutates session
response = session.send_request() # Session has methods
After (Functional - New)
core.register_provider(PluginClass) # Just class
config = {...} # App manages config
session = core.create_session() # Returns immutable Session
session = core.add_message(session, "user", "Hello") # Returns new session
session, response = core.send_request(session, config) # Pure function
Complete Example
from agent_core import AgentCore
from plugins.openai_provider import OpenAICompatibleProvider
from plugins import WebSearchFeature
# Setup (stateless pipeline)
core = AgentCore()
core.register_provider(OpenAICompatibleProvider)
core.register_feature(WebSearchFeature)
# Application manages state
config = {
"provider": "openai_compatible",
"model": "gpt-4",
"api_key": "sk-...",
"web_search_enabled": True,
}
# Functional conversation flow
session = core.create_session("user-123")
session = core.add_message(session, "system", "You are helpful")
# Chat loop
while True:
user_input = input("You: ")
if user_input.lower() == "quit":
break
# Functional updates
session = core.add_message(session, "user", user_input)
session, response = core.send_request(session, config)
print(f"Assistant: {response['content']}")
# Session grows immutably
print(f"Messages: {len(session.messages)}")
Version
Current version: 0.3.0 (Functional Architecture + Discovery)
License
Copyright 2026 Dynamic Programming Solutions Kft.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.