A lightweight AI Agent orchestration framework with unified Task/Prompt/Function management and parallel routing
Project description
Triad Core AI Framework Technical Manual
Version: v1.1
Use Case: Enterprise-grade AI Agent Development, Production Deployment
Core Features: Three-tier Memory Isolation, Dynamic Runtime Registration, Native MCP Support, DAG Task Orchestration
1. Product Overview
1.1 Introduction
Triad Core AI is a Python framework for building structured, multi-step AI Agent workflows. It provides a Task-based orchestration engine that lets you compose complex AI interactions into executable graphs made of composable, reusable tasks โ each with its own Prompt, tool functions, conversation context, and data flow.
The name "Triad" comes from the three core concepts managed through the central Registry: Task, Prompt, and Function, which together form the capability foundation of the framework.
1.2 Core Triad
| Concept | Description |
|---|---|
| Task | An independent unit encapsulating business logic and AI interactions; the smallest granularity of orchestration |
| Prompt | Jinja2-templated prompts with automatic variable extraction; 1:1 binding with Task |
| Function | Tools callable by AI, with parameters defined using JSON Schema; supports local functions and MCP remote tools |
1.3 Feature Highlights
- ๐งฉ Declarative Task Definition โ Define AI tasks with concise decorators (
@task,@prompt,@function), lowering the barrier to entry - ๐ Task Chain Routing โ Tasks can route to one or more downstream tasks via
next_task, forming a DAG execution graph - โก Parallel Execution โ Independent tasks in the same layer run concurrently via
asyncio, improving throughput - ๐ Automatic Tool Loop โ Built-in
call_ai_e2e()handles multi-turn tool call loops automatically, no manual dialog state management needed - ๐ฆ Multi-layer Context โ Supports session-level (cross-request), task-level (per-task private), and request-level (single-call temporary) data isolation
- ๐ฌ Intelligent Conversation History โ Task-isolated conversation history (auto-cleaned after task ends) + global semantic task history (cross-task long-term memory)
- ๐ Pluggable AI Providers โ Built-in OpenAI provider (compatible with OpenAI, DeepSeek, Azure, etc.); custom extensions supported
- โฑ High Availability โ Automatic retries with exponential backoff, configurable timeouts, suitable for unstable network environments
- ๐พ Session Persistence โ Supports in-memory storage (development) and Redis storage (production, including standalone/sentinel/cluster modes)
- ๐ Comprehensive Error Handling โ Full exception hierarchy for different failure modes, facilitating issue diagnosis and circuit-breaking
2. Core Concepts
This section dives into the details of the core triad and the surrounding concepts that power the framework:
2.1 Task
A Task is the encapsulation unit for business logic, divided into two categories:
- Base Task: Inherits from the
BaseTaskabstract class, suitable for deterministic business logic - AI Task: Inherits from the
AITaskbase class, with built-in AI invocation, tool loop, and context management capabilities, suitable for LLM-driven flexible logic
Each task has a unique task_id, and conversation history is automatically isolated during execution to prevent cross-task context contamination. Tasks declare their dependent tools via required_functions, which are automatically validated for existence at registration time.
2.2 Prompt
A Prompt is the AI's instruction template, using Jinja2 syntax with support for variable interpolation. The framework automatically extracts variables from the template and injects values at render time, in the following priority order:
- Parameters explicitly passed to the
render()method (highest priority) - Current task's private data (
Context.get()) - Global session data (
Context.get_session())
StrictUndefined Mode Enabled: If a variable is undefined, a RuntimeError is raised directly at render time, preventing the AI from receiving incorrect information.
2.3 Function
A Function is an atomic tool callable by AI, divided into two categories:
- Local Function: Python synchronous/asynchronous functions, bound via the
@functiondecorator or dynamic registration - MCP Tool: Remote tools accessed via the MCP protocol; the framework automatically converts them into internal Functions with the naming format
server_name.tool_name
Function parameters are defined using JSON Schema, and the AI automatically generates valid call parameters based on the Schema.
2.4 Supporting Core Concepts
| Concept | Scope | Description |
|---|---|---|
| Context | Request-level | State container for a single request, managing multi-layer data and conversation history |
| Registry | Global singleton | Central registry storing all Task, Prompt, and Function definitions; supports static and dynamic registration |
| Runtime | Request-level | DAG orchestration engine responsible for task scheduling, parallel execution, and chain progression |
| Agent | Task-level | Single-task executor handling retries, timeout control, and exception capture |
| Session | User-level | Cross-request persistent storage for user state and long-term preferences |
3. Project Structure
The framework adopts a modular design with a clear directory structure, facilitating secondary development and troubleshooting:
triad-core-ai/
โโโ src/
โ โโโ triad_core_ai/ # Framework core source code
โ โโโ __init__.py # Public API exports
โ โโโ ai_task.py # AITask base class: AI-driven tasks with built-in tool loop logic
โ โโโ exceptions.py # Global exception hierarchy definition
โ โโโ schemas.py # Pydantic data models: TaskInput, TaskOutput, etc.
โ โโโ task.py # BaseTask abstract class: parent class for all tasks
โ โโโ core/ # Core runtime modules
โ โ โโโ agent.py # Agent executor + AgentPool unified entry point
โ โ โโโ context.py # Multi-layer context container implementation
โ โ โโโ runtime.py # DAG task orchestration engine
โ โ โโโ session.py # Session persistence implementation (in-memory/Redis)
โ โโโ interfaces/ # Abstract interface definitions
โ โ โโโ ai_provider.py # AIProvider abstract base class, ToolCall, ChatResponse definitions
โ โโโ providers/ # AI provider implementations
โ โ โโโ mock_provider.py # Mock AI provider (for unit testing)
โ โ โโโ openai_provider.py # OpenAI-compatible provider (supports OpenAI/DeepSeek/Azure/etc.)
โ โโโ registry/ # Registry module (static + dynamic registration support)
โ โโโ decorators.py # Static decorator implementations
โ โโโ registry.py # Global Registry singleton + dynamic registration implementation
โโโ pyproject.toml # Project configuration file (dependencies, packaging, lint rules)
โโโ README.md # English documentation
โโโ README_CN.md # Chinese documentation
โโโ main.py # Example entry point
# Optional extension modules (import on demand)
โโโ triad_core_ai/
โโโ mcp/ # MCP protocol support extension
โโโ adapter.py # MCP Server adapter
โโโ handler.py # Unified MCP tool execution entry point
โโโ registry.py # MCP registry center
โโโ schema.py # MCP server configuration model
4. Registration Mechanism
โ ๏ธ Important: Dynamic registration is NOT exclusive to AI-generated scenarios. It is a "runtime programmable" capability suitable for admin dashboards, plugin architectures, hot-reload configurations, and any scenario requiring capability registration without service restart.
Static and dynamic registrations both write to the same global Registry and are fully interoperable.
4.1 Static Registration
Located in src/triad_core_ai/registry/decorators.py, suitable for hardened business logic with optimal performance.
Note: All statically registered functions, prompts, and tasks need to be imported in __init__ to be recognized by the runtime.
@function โ Registering Tool Functions
from triad_core_ai.registry.decorators import function
@function(
name="query_order", # Unique tool identifier; for MCP tools, use format: server_name.tool_name
description="Query order details by order number", # AI-readable tool description; clearer = higher accuracy
parameters={ # JSON Schema format, defining input constraints
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Order number, format: ORDxxxxxx"
},
"include_items": {
"type": "boolean",
"default": False,
"description": "Whether to return order item details"
}
},
"required": ["order_id"] # Required parameter list
}
)
def query_order(order_id: str, include_items: bool = False) -> dict:
"""Business logic implementation; supports sync/async functions"""
# Database query logic
return {"order_id": order_id, "status": "shipped"}
@prompt โ Registering System Prompts
from triad_core_ai.registry.decorators import prompt
@prompt(
name="customer_service_role", # Unique prompt identifier; defaults to Task name
description="E-commerce customer service role setting" # Prompt description
)
def cs_prompt():
"""Returns a Jinja2 template string; variables are auto-injected from Context"""
return """
You are an e-commerce customer service assistant. Current user ID: {{ user_id }}
Recent inquiry history: {{ recent_queries }}
Please reply in strict JSON format with fields: answer, need_transfer
"""
- Template variable injection priority:
render()explicit params >Context.task_data>Context.session_data - StrictUndefined enabled: Undefined variables raise errors immediately, preventing AI from receiving bad information
@task โ Registering Business Tasks
from triad_core_ai.registry.decorators import task
from triad_core_ai.ai_task import AITask
from triad_core_ai.schemas import TaskInput, TaskOutput
@task(
name="after_sale_process", # Unique task identifier
description="After-sales issue handling workflow", # Task description
required_functions=[ # Tools this task depends on; auto-validated at registration
"query_order",
"refund_order"
]
)
class AfterSaleTask(AITask):
async def execute(self, inp: TaskInput) -> TaskOutput:
# 1. Prepare context variables
self.context.set("recent_queries", inp.user_message)
# 2. Call AI (automatically handles Tool Loop)
response = await self.call_ai_e2e(
max_tool_loops=3, # Maximum tool call rounds
max_lazy_retries=2 # AI empty-reply retry count
)
# 3. Return result
return TaskOutput(
success=True,
ai_response=response.content,
data={"refund_id": self.context.get("refund_id")},
next_task=["notify_user"] if response.content else None, # Downstream tasks
user_visible=True, # Result visible to user
ai_visible=True # Result visible to AI (written to global memory)
)
4.2 Dynamic Registration
Located in src/triad_core_ai/registry/, suitable for admin configurations, plugin architectures, and hot-reload scenarios โ add capabilities without modifying code.
DynamicFunctionRegistry โ Dynamically Register Functions
Used to wrap existing business functions as AI tools, or to add new tool definitions at runtime.
from triad_core_ai.registry import get_dynamic_function_registry
# 1. Get the global dynamic function registry
dyn_func_reg = get_dynamic_function_registry()
# 2. Bind an existing business function (most common usage)
def legacy_refund_logic(order_id: str, amount: float):
"""Existing business function; no need to modify original code"""
return {"refund_id": "RFxxxxxx", "status": "success"}
dyn_func_reg.register(
name="legacy_refund", # Tool name
handler=legacy_refund_logic, # Bind existing execution function
description="Interface to legacy refund system",
parameters={
"type": "object",
"properties": {
"order_id": {"type": "string"},
"amount": {"type": "number"}
},
"required": ["order_id", "amount"]
}
)
# 3. Register definition only (no custom handler, uses default placeholder)
# Suitable for scenarios where the handler will be bound later via other mechanisms
dyn_func_reg.register(
name="temp_tool",
handler=lambda **kwargs: f"Temp tool called: {kwargs}",
description="Temporary tool with default handler",
parameters={"type": "object", "properties": {"x": {"type": "string"}}}
)
DynamicTaskRegistry โ Dynamically Register Tasks
Used to add tasks at runtime; the framework automatically performs prompt validation and Task class generation.
from triad_core_ai.registry import get_dynamic_task_registry
from triad_core_ai.dynamic.schemas import DynamicTaskDefinition, DynamicFunctionSpec
# 1. Get the global dynamic task registry
dyn_task_reg = get_dynamic_task_registry()
# 2. Define the task (essentially a Pydantic model, can be parsed directly from API request body)
task_def = DynamicTaskDefinition(
task_name="dynamic_after_sale", # Task name, should match Prompt name
prompt_template="""User question: {{ user_question }}
Please call query_order to check order status, then decide whether to refund.""", # Jinja2 template
required_functions=["query_order"], # Dependencies on already-registered functions
new_functions=[ # New function definitions (optional)
DynamicFunctionSpec(
name="query_order",
description="Query order",
parameters={"properties": {"order_id": {"type": "string"}}}
)
],
timeout=60, # Task timeout in seconds
max_tool_loops=3, # Maximum tool call rounds
max_lazy_retries=2 # AI empty-reply retry count
)
# 3. Register (automatically validates Prompt variables against function parameters)
registered_name = dyn_task_reg.register(task_def)
print(f"Dynamic task registered successfully: {registered_name}")
- Automatic validation at registration: Checks whether variables referenced in the Prompt exist in the parameters of
required_functions - Automatic generation: The framework automatically creates a dynamic Task class inheriting from
AITask, no manual class definition needed
5. Session Storage Configuration
Session storage is responsible for persisting session_data (user login state, history preferences, business state, etc.) across requests. Both in-memory and Redis backends are supported.
5.1 In-Memory Storage (Development Environment)
Default configuration; data is stored in the Python process memory with no additional dependencies required.
from triad_core_ai.core.session import InMemorySessionStore
session_store = InMemorySessionStore()
# No additional parameters; suitable for single-process development and debugging
# Drawbacks: Data lost on service restart; not shared across multi-process/multi-instance deployments
5.2 Redis Storage (Production Environment, Recommended)
Located in src/triad_core_ai/core/session.py, supports standalone, sentinel, and cluster modes. Install the dependency first:
pip install redis>=4.0
Basic Configuration (Standalone Redis)
from triad_core_ai.core.session import RedisSessionStore
session_store = RedisSessionStore(
url="redis://:password@localhost:6379/0", # Redis connection string
prefix="triad:session:", # Key prefix for multi-app isolation
ttl=3600 # Session expiration time in seconds (default: 1 hour)
)
Production-Grade Configuration (Custom Connection Pool)
import redis.asyncio as aioredis
from triad_core_ai.core.session import RedisSessionStore
# 1. Custom Redis client (supports connection pooling, SSL, timeouts, etc.)
redis_client = aioredis.Redis(
host="redis-host",
port=6379,
password="password",
db=0,
decode_responses=True, # Auto-decode to strings; no manual bytes-to-str conversion
max_connections=100, # Connection pool size; adjust based on QPS
socket_timeout=5, # Socket timeout
socket_connect_timeout=5, # Connection timeout
health_check_interval=30 # Connection health check interval
)
# 2. Inject custom client
session_store = RedisSessionStore(
url="redis://ignored", # URL is ignored when a custom client is injected
prefix="triad:prod:session:",
ttl=7200 # Production environments should use TTL >= 2 hours
)
session_store._client = redis_client
Sentinel Mode Configuration
from redis.asyncio.sentinel import Sentinel
from triad_core_ai.core.session import RedisSessionStore
# 1. Configure sentinel
sentinel = Sentinel(
sentinels=[("sentinel-1", 26379), ("sentinel-2", 26379)],
password="sentinel-password",
socket_timeout=5
)
# 2. Get master node client
master = sentinel.master_for(
service_name="mymaster",
password="redis-password",
decode_responses=True,
max_connections=100
)
# 3. Inject client
session_store = RedisSessionStore(url="redis://ignored")
session_store._client = master
Cluster Mode Configuration
from redis.asyncio.cluster import RedisCluster
from triad_core_ai.core.session import RedisSessionStore
# 1. Configure cluster
redis_cluster = RedisCluster(
host="redis-cluster-host",
port=7000,
password="cluster-password",
decode_responses=True,
max_connections=100
)
# 2. Inject client
session_store = RedisSessionStore(url="redis://ignored")
session_store._client = redis_cluster
Integrating Session Storage in AgentPool
from triad_core_ai.core.agent_pool import AgentPool
pool = AgentPool(
session_id="user_12345", # Unique user identifier, used as Redis key suffix
ai_provider=ai_provider, # AI provider instance
registry=registry, # Global registry
session_store=session_store, # Redis session store instance
session_data={}, # Initial data (overwritten if exists in Redis)
mcp_servers=[] # MCP server configurations (optional)
)
# After each process() call, the framework automatically saves Context.session_data to Redis
result = await pool.process(
user_message="Check my orders",
initial_task="after_sale_process"
)
6. Orchestration Mechanism
6.1 Core Dependency Relationships
These are strongly bound relationships, with dependency validity automatically verified at registration:
- Task โ depends on โ Prompt (1:1 binding; default: Task name = Prompt name)
- Task โ depends on โ Function (1:N binding via
required_functions) - Prompt โ references โ Context variables (no strong dependency; runtime-checked)
6.2 Complete Execution Flow
โโ User Request โโ AgentPool.process()
โ
โโ 1. Load Session: Load session_data from SessionStore into Context
โ
โโ 2. Initialize Runtime: Create Context, generate runtime_task_id, write user message
โ
โโ 3. DAG Scheduling: current_layer = [initial_task]
โ
โโ 4. Parallel Execute Current Layer: asyncio.gather(*tasks)
โ โ
โ โโ Agent.run(): Handle retries/timeouts
โ โ โ
โ โ โโ AITask.execute(): Business logic entry point
โ โ โ
โ โ โโ Build Prompt: Inject variables from Context
โ โ โ
โ โ โโ Assemble Messages:
โ โ โ โโ System Prompt (Task-bound Prompt)
โ โ โ โโ Task History (global semantic memory, ai_visible=True)
โ โ โ โโ Conversation History (current Task isolated)
โ โ โ โโ User Message
โ โ โ
โ โ โโ call_ai_e2e(): AI invocation + Tool Loop
โ โ โ โโ Call AI API
โ โ โ โโ If tool_calls exist: Execute functions (local/MCP)
โ โ โ โโ Write tool results to Conversation History
โ โ โ โโ Repeat until AI returns final text
โ โ โ
โ โ โโ Archive: Write result to Task History
โ โ
โ โโ Collect TaskOutput, update session_data
โ
โโ 5. Chain Progression: If TaskOutput.next_task is non-empty, add to next layer's execution queue
โ
โโ 6. Persist: Save Context.session_data back to SessionStore, return result
6.3 Chained Task Scheduling
Task relay is implemented via TaskOutput.next_task, supporting serial, parallel, and conditional branching:
class OrderQueryTask(AITask):
async def execute(self, inp: TaskInput) -> TaskOutput:
order_id = inp.user_message.split()[-1]
order_info = await query_order(order_id)
# Store order status in session for downstream tasks
self.context.set_session("order_status", order_info["status"])
# Determine downstream tasks based on status
if order_info["status"] == "shipped":
next_tasks = ["logistics_query"]
elif order_info["status"] == "pending":
next_tasks = ["remind_pay"]
else:
next_tasks = []
return TaskOutput(
success=True,
data={"order_info": order_info},
next_task=next_tasks
)
- Best practice for data passing: Cross-task data should be shared via
Context.set_session()/get_session(), notprevious_output, to avoid data races in parallel scenarios - Parallel execution: Multiple tasks in the same layer execute concurrently, ideal for independent tasks (e.g., querying orders and user info simultaneously)
6.4 Memory Isolation Model
The framework achieves four-tier memory isolation toๅฝปๅบ eliminate cross-task context contamination:
โโ Session Store (Redis) โโโโโโโโโโโโโโโโโโโโโโ
โ session_data: {user_id:123, token:"xxx"} โ # Globally shared, cross-request persistent
โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ Load/Save
โโ Context (Single Request) โโโโโโโโโโโโโโโโโโโ
โ โโ session_data: Copy from Session Store โ # Globally shared, readable/writable within request
โ โโ task_data: Temporary vars, destroyed after Task ends โ # Task-private
โ โโ task_history: [...] โ # Global semantic memory, shared across Tasks
โ โโ conversation_histories: { โ # Task-isolated conversation history
โ "task_1": [sys, user, asst, tool], โ # Visible only to Task 1
โ "task_2": [sys, user, asst], โ # Visible only to Task 2
โ "runtime_task_id": [...] โ # Root Runtime task history
โ }
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Conversation History: Strictly isolated by
task_id, ensuring multi-turn conversation contexts don't interfere with each other - Task History: Globally shared semantic summary; only stores results with
ai_visible=True, used to inject long-term memory into AI - Auto-cleanup: Conversation History for a Task is automatically cleaned up after the Task ends, preventing memory leaks
7. API Quick Reference
Core Class Initialization Parameters
Agent
| Parameter | Type | Default | Description |
|---|---|---|---|
task_def |
TaskDefinition | Required | Task definition |
ai_provider |
AIProvider | Required | AI provider instance |
context |
Context | Required | Context instance |
registry |
Registry | Required | Global registry |
max_retries |
int | 3 | Retry count for timeouts/network errors |
timeout |
int | 30 | Single-task timeout in seconds |
Runtime
| Parameter | Type | Default | Description |
|---|---|---|---|
user_message |
str | Required | Original user input |
registry |
Registry | Required | Global registry |
ai_provider |
AIProvider | Required | AI provider instance |
session_data |
dict | Required | Session data |
task |
str | Required | Root task name |
max_iterations |
int | 50 | Maximum task chain depth |
wait_timeout |
int | 120 | Task wait timeout in seconds |
AgentPool
| Parameter | Type | Default | Description |
|---|---|---|---|
session_id |
str | Required | Unique user identifier |
ai_provider |
AIProvider | Required | AI provider instance |
registry |
Registry | Required | Global registry |
session_store |
SessionStore | None | Session store instance |
session_data |
dict | {} |
Initial session data |
mcp_servers |
List[dict] |
[] |
MCP server configuration list |
OpenAIProvider
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key |
str | Required | API key |
base_url |
str | Official OpenAI URL | Compatible endpoint (DeepSeek/Azure/Ollama, etc.) |
model |
str | gpt-4o-mini | Model name |
enable_json_mode |
bool | True | Whether to enable JSON output mode |
timeout |
float | 60.0 | Request timeout in seconds |
max_retries |
int | 3 | API call retry count |
Common Context Methods
| Method | Parameters | Return | Description |
|---|---|---|---|
set(key, value) |
key: str, value: Any |
None | Set Task-level temporary variable |
get(key) |
key: str |
Any | Get Task-level temporary variable |
set_session(key, value) |
key: str, value: Any |
None | Set Session-level persistent variable |
get_session(key) |
key: str |
Any | Get Session-level persistent variable |
add_conversation_entry(...) |
role, content, task_id, etc. |
None | Write to specified Task's conversation history |
add_task_history_entry(...) |
task_name, content, user_visible, ai_visible |
None | Write to global semantic memory |
get_recent_task_history(n=3) |
n: int |
List[dict] |
Get the last n entries from global semantic memory |
8. Practical Examples
Example 1: Standard Static Registration (Full Workflow)
import asyncio
from triad_core_ai.registry.decorators import function, prompt, task
from triad_core_ai.ai_task import AITask
from triad_core_ai.schemas import TaskInput, TaskOutput
from triad_core_ai.providers.openai_provider import OpenAIProvider
from triad_core_ai.core.runtime import Runtime
from triad_core_ai.core.session import InMemorySessionStore
# 1. Define the tool
@function(
name="calc_sum",
description="Calculate the sum of two numbers",
parameters={"properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]}
)
def calc_sum(a: float, b: float) -> float:
return a + b
# 2. Define the Prompt
@prompt(name="calc_role")
def calc_prompt():
return "You are a calculator. Call the calc_sum tool to compute the sum of two numbers provided by the user. Return the result in JSON format."
# 3. Define the Task
@task(name="calc_task", required_functions=["calc_sum"])
class CalcTask(AITask):
async def execute(self, inp: TaskInput) -> TaskOutput:
response = await self.call_ai_e2e()
return TaskOutput(success=True, ai_response=response.content, user_visible=True)
# 4. Run
async def main():
ai_provider = OpenAIProvider(api_key="sk-xxx")
registry = get_registry() # Global registry singleton
session_store = InMemorySessionStore()
runtime = Runtime(
user_message="Calculate 3.5 plus 4.2",
registry=registry,
ai_provider=ai_provider,
session_data={},
task="calc_task"
)
result = await runtime.run()
print(result["outputs"]["calc_task"]["ai_response"])
if __name__ == "__main__":
asyncio.run(main())
Example 2: Dynamic Registration (Admin Config Scenario, Not AI-Generated)
import asyncio
from triad_core_ai.registry import get_dynamic_task_registry
from triad_core_ai.dynamic.schemas import DynamicTaskDefinition
from triad_core_ai.providers.openai_provider import OpenAIProvider
from triad_core_ai.core.runtime import Runtime
from triad_core_ai.core.session import RedisSessionStore
from triad_core_ai.registry.registry import get_registry
# Simulate loading config from database
async def load_task_config_from_db():
return {
"task_name": "db_configured_task",
"prompt_template": "User question: {{ question }}. Please call knowledge_base to find the answer.",
"required_functions": ["knowledge_base"],
"timeout": 45
}
async def main():
# 1. Initialize core components
ai_provider = OpenAIProvider(api_key="sk-xxx")
registry = get_registry()
session_store = RedisSessionStore(url="redis://localhost:6379/0")
# 2. Load config from database and dynamically register task (no service restart needed)
config = await load_task_config_from_db()
dyn_task_reg = get_dynamic_task_registry()
dyn_task_reg.register(DynamicTaskDefinition(**config))
print("Dynamic task registration complete")
# 3. Run the dynamically registered task
runtime = Runtime(
user_message="How do I return an item?",
registry=registry,
ai_provider=ai_provider,
session_data={},
task="db_configured_task"
)
result = await runtime.run()
print(result["outputs"]["db_configured_task"]["ai_response"])
if __name__ == "__main__":
asyncio.run(main())
Example 3: MCP Tool Integration
import asyncio
from triad_core_ai.mcp.schema import MCPServerConfig
from triad_core_ai.mcp.registry import MCPRegistry
from triad_core_ai.providers.openai_provider import OpenAIProvider
from triad_core_ai.core.agent_pool import AgentPool
from triad_core_ai.core.session import RedisSessionStore
from triad_core_ai.registry.registry import get_registry
async def main():
# 1. Initialize core components
ai_provider = OpenAIProvider(api_key="sk-xxx")
registry = get_registry()
session_store = RedisSessionStore(url="redis://localhost:6379/0")
# 2. Configure MCP server (official filesystem server)
mcp_config = MCPServerConfig(
name="filesystem",
transport="stdio",
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
enabled=True
)
# 3. Register MCP server (automatically converts MCP tools to internal Functions, naming: filesystem.read_file)
mcp_registry = MCPRegistry()
await mcp_registry.register_server(mcp_config)
# 4. Initialize AgentPool
pool = AgentPool(
session_id="user_123",
ai_provider=ai_provider,
registry=registry,
session_store=session_store,
mcp_servers=[mcp_config.model_dump()]
)
# 5. Execute task (assumes a task exists that calls filesystem tools)
result = await pool.process(
user_message="Read the contents of /tmp/test.txt",
initial_task="file_read_task"
)
print(result)
if __name__ == "__main__":
asyncio.run(main())
9. Best Practices
-
Registration mode selection:
- Use static registration for core business logic and high-frequency capabilities โ better performance and type safety
- Use dynamic registration for admin configurations, plugin architectures, and hot-reload scenarios
-
Session storage guidelines:
- Production environments MUST use Redis; set TTL based on business needs (recommend >= 2 hours for customer service scenarios)
- Use consistent key prefix conventions to avoid conflicts across applications
-
Prompt writing guidelines:
- Clearly define the AI's role, task, and output format
- Split complex logic into multiple Tasks; avoid overly long single Prompts
- When JSON mode is enabled, explicitly require the AI to return JSON conforming to the Schema
-
Error handling:
- On task execution failure, return
success=Falsewith a clearerrormessage - Tool call failures should return structured error information to help the AI understand the failure reason
- On task execution failure, return
-
Performance optimization:
- Minimize unnecessary Task History entries to avoid excessive context length
- Batch tool calls into a single AI invocation where possible
- Reuse MCP connections; avoid้ข็น creating and destroying connections
-
Parallel safety:
- Cross-task data sharing MUST go through
session_data; do not modifytask_dataand read it across tasks - Avoid multiple parallel tasks modifying the same
session_datakey simultaneously; use distributed locks when necessary
- Cross-task data sharing MUST go through
10. Exception Hierarchy
The framework provides fine-grained exception classification for implementing circuit-breaking, retries, and graceful degradation across different error scenarios:
TaskRouterError # Base framework exception (parent of all exceptions)
โโโ TaskRegistrationError # Dynamic task/Prompt registration failure (e.g., validation error)
โโโ TaskExecutionError # Task execution failure (business logic error or AI call exception)
โโโ TaskNotFoundError # Specified task not found in registry
โโโ FunctionNotFoundError # Specified function not found in registry
โโโ PromptNotFoundError # Specified prompt not found in registry
โโโ DependencyCycleError # Circular dependency detected in task graph (DAG validation failed)
โโโ RetryExhaustedError # Retry attempts exhausted (timeout/network error retries failed)
โโโ ContextError # Context operation exception (e.g., undefined variable, read-write conflict)
โโโ ProviderUnavailableError # AI provider unavailable (e.g., invalid API key, service outage)
11. FAQ
Q1: What if a dynamically registered function has no custom handler?
A: The framework will use a default_handler (returns call parameter logs). For production, it is recommended to register a handler in advance or bind an existing function during dynamic registration.
Q2: How do I troubleshoot Redis connection failures?
A:
- Verify Redis address, port, and password are correct
- Check network connectivity and firewall rules
- Enable DEBUG logging to inspect Redis connection error messages
- Verify Redis service is running:
redis-cli ping
Q3: What if a task gets stuck in a Tool Loop?
A:
- Increase the
max_tool_loopsparameter (default is 3) - Check if the tool execution logic has an infinite loop
- Verify that AI-returned
tool_callsparameters conform to the function Schema - Enable DEBUG logging to inspect tool execution progress and return values
Q4: How do I make the AI remember long-term information across Tasks?
A: Write information that needs to be remembered long-term via Context.add_task_history_entry(ai_visible=True) to the global semantic memory. The AI will automatically read the most recent 3 entries in subsequent calls.
Q5: What should I watch out for in multi-process deployments?
A:
- You MUST use
RedisSessionStore; in-memory storage is not supported - MCP connections should use short-lived connections, or ensure connection isolation across processes
- Dynamic registration operations require distributed locks to prevent duplicate registration
- Avoid modifying the same global Registry configuration across multiple processes
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file triad_core_ai-0.1.0.tar.gz.
File metadata
- Download URL: triad_core_ai-0.1.0.tar.gz
- Upload date:
- Size: 67.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd7b6319052cf562bdd9dd098c23721eddca5891b07e4ce654179b72e43f5eaa
|
|
| MD5 |
2c66d35f7a3b510ab2b6b78e2e3cc35a
|
|
| BLAKE2b-256 |
bffdfeff39b2bd8c141643d02f51e8a16217bf9f176fdd400c14d07935bc6e57
|
File details
Details for the file triad_core_ai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: triad_core_ai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d54c28649ec9d0b5320f17a867e079043d95e8f65a4d8b1d616382896abebe05
|
|
| MD5 |
e7a7f4c7a16e45a945e1184ffbdf7dce
|
|
| BLAKE2b-256 |
35e11910401a1649a194d1b04a111a4cf26678fdb81a7c12b881b4da1f7bb267
|