Architecture Design¶
apcore internal architecture and component interactions.
1. Overall Architecture¶
apcore's architecture comprises two orthogonal dimensions: framework technical layers (vertical) and suggested business layers (horizontal).
1.1 Framework Technical Layers (Vertical)¶
The framework's own technical layering, defining the complete flow from module registration to execution:
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ HTTP API │ │ CLI │ │ MCP Server │ │
│ └────────┬────────┘ └────────┬────────┘ └───────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ ▼ │
├─────────────────────────────────────────────────────────────────┤
│ Execution Layer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Executor │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │ ACL │ │Validate │ │Middleware│ │ Execute │ │ │
│ │ │ Check │→ │ Input │→ │ Chain │→ │ Module │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ Look up module │
├─────────────────────────────────┼───────────────────────────────┤
│ Registry Layer │
│ ┌─────────────────────────────┴─────────────────────────────┐ │
│ │ Registry │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Discover │ │ ID Map │ │ Validate │ │ Modules │ │ │
│ │ │ │→ │ │→ │ │→ │ Store │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ Read │
├─────────────────────────────────┼───────────────────────────────┤
│ Module Layer │
│ ┌─────────────────────────────┴─────────────────────────────┐ │
│ │ User-written business modules (Module interface) │ │
│ │ (extensions/) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
1.2 Suggested Business Layers (Horizontal)¶
In the extensions/ directory, it is recommended to divide modules by responsibility (enforced by ACL):
extensions/
│
├── api/ # API Layer: handles external requests
│ ├── handler/
│ └── ACL: can only call orchestrator.*
│
├── orchestrator/ # Orchestration Layer: composes business workflows
│ ├── workflow/
│ └── ACL: can only call executor.* and common.*
│
├── executor/ # Execution Layer: concrete business operations
│ ├── email/
│ ├── sms/
│ ├── database/
│ └── ACL: can call common.*, can connect to external systems
│
└── common/ # Common Layer: utility and helper functions
├── util/
└── ACL: read-only operations, called by all layers
Key Points: - Framework technical layers (Application → Execution → Registry → Module) are apcore's implementation mechanism - Business layers (api → orchestrator → executor → common) are best practice recommendations, enforced by ACL configuration - Both are orthogonal: modules in any business layer go through the same framework layers
2. Core Components¶
2.1 Module¶
Modules are the smallest execution unit in apcore.
class Module(ABC):
"""Module base class"""
# ====== Core Layer (Required) ======
input_schema: ClassVar[Type[BaseModel]]
output_schema: ClassVar[Type[BaseModel]]
description: ClassVar[str]
# ====== Annotation Layer (Optional) ======
name: ClassVar[str | None]
tags: ClassVar[list[str]]
version: ClassVar[str]
annotations: ClassVar[ModuleAnnotations | None] # Behavior annotations
examples: ClassVar[list[ModuleExample]] # Usage examples
# ====== Extension Layer (Optional) ======
metadata: ClassVar[dict[str, Any]] # Free metadata
# Core methods (def or async def both supported, framework auto-detects)
def execute(self, inputs: dict, context: Context) -> dict: ...
def validate(self, inputs: dict) -> ValidationResult: ...
# Lifecycle
def on_load(self) -> None: ...
def on_unload(self) -> None: ...
Responsibilities: - Define input/output Schema - Implement concrete business logic - Manage its own resources (connection pools, etc.)
2.2 Registry¶
Registry is responsible for module discovery, registration, and management.
┌─────────────────────────────────────────────────────────────┐
│ Registry │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Discoverer │────▶│ ID Map │────▶│ Validator │ │
│ └──────────────┘ └──────────────┘ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Module Store │ │
│ │ │ │
│ │ module_id │ module_class │ instance │ │
│ │ ─────────────────┼────────────────┼────────────── │ │
│ │ executor.email │ SendEmail... │ <instance> │ │
│ │ executor.sms │ SendSMS... │ <instance> │ │
│ │ ... │ ... │ ... │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Component Description:
| Component | Responsibility |
|---|---|
| Discoverer | Scan extensions directory, find Module subclasses |
| ID Map | Handle cross-language ID conversion |
| Validator | Verify modules correctly implement interface |
| Module Store | Store module classes and instances |
2.3 Executor¶
Executor is responsible for module invocation and execution.
┌─────────────────────────────────────────────────────────────┐
│ Executor │
├─────────────────────────────────────────────────────────────┤
│ │
│ call(module_id, inputs, context) │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 1. Context Processing │ │
│ │ - Create/validate Context │ │
│ │ - Update caller_id, call_chain │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 2. Lookup Module │ │
│ │ - registry.get(module_id) │ │
│ │ - Throw ModuleNotFoundError if not found │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 3. ACL Check │ │
│ │ - Check caller → target permission │ │
│ │ - Throw ACLDeniedError if rejected │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 4. Input Validation │ │
│ │ - Validate against input_schema │ │
│ │ - Throw ValidationError on failure │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 5. Middleware before │ │
│ │ - Execute middleware.before() in order │ │
│ │ - Can modify inputs │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 6. Module Execution │ │
│ │ - module.execute(inputs, context) │ │
│ │ - Call middleware.on_error() on exception │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 7. Output Validation │ │
│ │ - Validate against output_schema │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 8. Middleware after │ │
│ │ - Execute middleware.after() in reverse order │ │
│ │ - Can modify output │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ ▼ │
│ Return result │
│ │
└─────────────────────────────────────────────────────────────┘
2.4 Context¶
Context flows through the entire call chain, carrying tracing and user information.
┌─────────────────────────────────────────────────────────────┐
│ Context │
├─────────────────────────────────────────────────────────────┤
│ │
│ trace_id: "abc-123" # Unique call chain ID │
│ caller_id: "orchestrator.x" # Calling module ID │
│ call_chain: ["a", "b", "c"] # Complete call path │
│ executor: <Executor> # Executor reference │
│ identity: Identity(...) # Caller identity (ACL uses) │
│ data: {...} # Shared pipeline state │
│ │
└─────────────────────────────────────────────────────────────┘
Context Propagation:
Top-level call (trace_id: "abc", caller_id: None, call_chain: [])
│
▼
Module A (trace_id: "abc", caller_id: None, call_chain: ["A"])
│
├── context.executor.call("B", inputs, context)
│ │
│ ▼
│ Module B (trace_id: "abc", caller_id: "A", call_chain: ["A", "B"])
│ │
│ ├── context.executor.call("C", inputs, context)
│ │ │
│ │ ▼
│ │ Module C (trace_id: "abc", caller_id: "B", call_chain: ["A", "B", "C"])
│ │
│ └── Return
│
└── Return
2.5 ACL (Access Control)¶
ACL controls call permissions between modules.
┌─────────────────────────────────────────────────────────────┐
│ ACL │
├─────────────────────────────────────────────────────────────┤
│ │
│ Rules (match in order): │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. callers: ["admin.*"] targets: ["*"] effect: allow│ │
│ │ 2. callers: ["api.*"] targets: ["executor.*"] effect: deny │ │
│ │ 3. callers: ["orch.*"] targets: ["executor.*"] effect: allow│ │
│ │ 4. callers: ["*"] targets: ["common.*"] effect: allow│ │
│ │ 5. callers: ["*"] targets: ["*"] effect: deny │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ check(caller_id, target_id) -> bool │
│ │
└─────────────────────────────────────────────────────────────┘
Matching Process:
check("api.handler", "executor.email")
Rule 1: "admin.*" vs "api.handler" → No match
Rule 2: "api.*" vs "api.handler" → Match!
"executor.*" vs "executor.email" → Match!
effect: deny → Return False
2.6 Middleware¶
Middleware executes in an onion model.
┌─────────────────────────────────────────────────────────────┐
│ Middleware Chain │
├─────────────────────────────────────────────────────────────┤
│ │
│ Request ──┐ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ MW1.before ──────────────────────────────────┐ ││
│ │ ┌─────────────────────────────────────────┐ │ ││
│ │ │ MW2.before ───────────────────┐ │ │ ││
│ │ │ ┌──────────────────────────┐ │ │ │ ││
│ │ │ │ MW3.before ────┐ │ │ │ │ ││
│ │ │ │ ┌────────────┐│ │ │ │ │ ││
│ │ │ │ │ Module ││ │ │ │ │ ││
│ │ │ │ │ execute() ││ │ │ │ │ ││
│ │ │ │ └────────────┘│ │ │ │ │ ││
│ │ │ │ MW3.after ─────┘ │ │ │ │ ││
│ │ │ └──────────────────────────┘ │ │ │ ││
│ │ │ MW2.after ────────────────────┘ │ │ ││
│ │ └─────────────────────────────────────────┘ │ ││
│ │ MW1.after ───────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ Response ── │
│ │
└─────────────────────────────────────────────────────────────┘
3. Data Flow¶
3.1 Module Discovery Flow¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ extensions/ │────▶│ Discoverer │────▶│ ID Map │
│ structure │ │ scan files │ │ convert ID │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Module │◀────│ on_load() │◀────│ Validator │
│ Store │ │ initialize │ │ verify │
└─────────────┘ └─────────────┘ └─────────────┘
3.2 Module Call Flow¶
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
│ Client │────▶│Executor │────▶│ ACL │────▶│ Validate │
│ │ │ .call() │ │ .check()│ │ inputs │
└─────────┘ └─────────┘ └─────────┘ └──────────┘
│
▼
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ Return │◀────│Middleware │◀────│Validate │◀────│ Module │
│ result │ │ .after │ │ output │ │ .execute │
└─────────┘ └──────────┘ └─────────┘ └──────────┘
▲
│
┌──────────┐
│Middleware │
│ .before │
└──────────┘
3.3 Error Handling Flow¶
Module.execute() throws exception
│
▼
MW3.on_error() ────── Has return? ─── Yes ──▶ Use as result
│
│ No
▼
MW2.on_error() ────── Has return? ─── Yes ──▶ Use as result
│
│ No
▼
MW1.on_error() ────── Has return? ─── Yes ──▶ Use as result
│
│ No
▼
Throw ModuleError
4. Directory Structure¶
4.1 Framework Source Structure¶
apcore/
├── __init__.py # Public API
├── module.py # Module base class
├── registry.py # Registry implementation
├── executor.py # Executor implementation
├── context.py # Context definition
├── acl.py # ACL implementation
├── schema.py # Schema utilities
├── errors.py # Exception definitions
├── config.py # Config loading
│
├── discovery/ # Module discovery
│ ├── __init__.py
│ ├── discoverer.py # Discoverer interface
│ ├── python.py # Python discoverer
│ └── id_map.py # ID Map handling
│
├── validation/ # Validation
│ ├── __init__.py
│ ├── module.py # Module interface validation
│ └── schema.py # Schema validation
│
├── middleware/ # Middleware
│ ├── __init__.py # Middleware base class + public API
│ ├── logging.py # Logging middleware
│ ├── metrics.py # Metrics middleware
│ └── retry.py # Retry middleware
│
└── utils/ # Utilities
├── __init__.py
└── pattern.py # Wildcard matching
4.2 Application Project Structure¶
my-project/
├── apcore.yaml # Framework config
├── extensions/ # Extensions directory
│ ├── api/ # API entry layer
│ │ └── handler/
│ ├── orchestrator/ # Orchestration layer
│ │ └── workflow/
│ ├── executor/ # Execution layer
│ │ ├── email/
│ │ ├── sms/
│ │ └── database/
│ └── common/ # Common components
│ └── util/
├── acl/ # Permission config
│ └── global_acl.yaml # ACL config
├── schemas/ # External schemas (optional)
│ └── email.yaml
└── config/ # Other config (optional)
└── id_map.yaml # ID Map (optional)
5. Extension Points¶
5.1 Custom Discoverer¶
from apcore.discovery import ModuleDiscoverer
class RemoteDiscoverer(ModuleDiscoverer):
"""Discover modules from remote service"""
def discover(self, config: dict) -> list[tuple[str, Type[Module]]]:
# Get module definitions from remote service
response = requests.get(config["remote_url"])
modules = []
for item in response.json():
module_class = self._build_module(item)
modules.append((item["id"], module_class))
return modules
5.2 Custom Validator¶
from apcore.validation import ModuleValidator
class StrictValidator(ModuleValidator):
"""Strict module validator"""
def validate(self, module_class: Type[Module]) -> list[str]:
errors = super().validate(module_class)
# Custom rules
if len(module_class.tags) == 0:
errors.append("Module must have at least one tag")
return errors
5.3 Custom ACL¶
from apcore import ACL
class RBACAuthorizer(ACL):
"""Role-based access control"""
def check(self, caller_id: str, target_id: str, context: Context) -> bool:
if not context.identity:
return False
required_roles = self._get_required_roles(target_id)
return bool(set(context.identity.roles) & set(required_roles))
6. Design Principles¶
6.1 Schema-Driven¶
All modules must define explicit schemas, ensuring: - AI/LLM can understand module functionality - Automatic validation of inputs/outputs - Automatic documentation and SDK generation
6.2 Zero-Configuration Priority¶
- Directory as ID: file paths automatically become module IDs
- Auto-discovery: scan directories to auto-register modules
- Convention over configuration: reasonable defaults
6.3 Pluggable¶
- Middleware system: flexibly extend execution flow
- Custom discoverers: support multiple module sources
- Custom ACL: support complex permission models
6.4 Observable¶
- trace_id flows through call chain
- Built-in metrics collection
- Structured logging support
7. Concurrency Model¶
7.1 Thread Safety Guarantees¶
| Component | Thread Safety Level | Description |
|---|---|---|
| Registry (read) | Fully safe | get(), has(), list() can be called concurrently |
| Registry (write) | Needs sync | register(), unregister() need locks |
| Executor | Fully safe | call() and call_async() can be called concurrently |
| Context | Partially safe | Immutable fields safe, data needs caller sync |
| ACL | Fully safe | Read-only checks, rules immutable after loading |
| Middleware | Must ensure | Instance methods must be thread-safe |
7.2 Concurrent Execution Model¶
Single call: serial execution
Executor.call() → ACL → Validate → Before MW → Execute → After MW → Return
Concurrent calls: independent contexts per call
Thread 1: Executor.call(A) → [independent ACL/Validate/MW chain]
Thread 2: Executor.call(B) → [independent ACL/Validate/MW chain]
Thread 3: Executor.call(C) → [independent ACL/Validate/MW chain]
Shared resources:
- Registry (read-only, safe)
- ACL rules (read-only, safe)
- Middleware instances (must be thread-safe)
- Module instances (execute() must be thread-safe)
Module Instance Lifecycle:
- Singleton model: Each
module_idcorresponds to unique instance,on_load()called only once - Reentrancy:
execute()must support multi-threaded concurrent calls - Internal module state should use locks or atomic variables for protection
Hot Reload Safety:
- During
unregister(), module may be executing in other threads - Implementation must maintain reference count, wait for execution completion before unload
- After timeout (default 30 seconds), force unload and log error
- See PROTOCOL_SPEC §11.7.3 Hot Reload Race Conditions
Middleware Chain Atomicity:
- Each
call()'s middleware chain (before → execute → after) doesn't interleave with other calls - Middleware instances shared at application level, must be thread-safe
- Call-level state should be stored in
context.data, not instance variables
7.3 Sync/Async Mixing¶
apcore supports mixed sync/async module calls, with automatic bridging:
| Caller | Called Module | Bridging Strategy |
|---|---|---|
| Sync | Sync | Direct call |
| Sync | Async | Block wait (await) |
| Async | Sync | Thread pool offload |
| Async | Async | Direct await |
Python Example:
# Async calling sync module (thread pool offload)
async def async_caller():
executor = Executor()
result = await executor.call_async("sync_module", {}) # Auto offload to thread pool
# Sync calling async module (blocking wait)
def sync_caller():
executor = Executor()
result = executor.call("async_module", {}) # Auto await
Note: Sync→async bridging blocks caller thread, should avoid frequent use in async contexts.
7.4 Timeout and Cancellation¶
Timeout Levels:
- Global timeout: includes before + execute + after (default 60 seconds)
- ACL check timeout: independent timing (default 1 second)
- Timing starts from first
before()middleware
Cancellation Strategy:
- Cooperative cancellation (recommended):
- Module checks
context.cancel_token.is_cancelled()and actively exits -
Suitable for long-running tasks (loops, I/O, etc.)
-
Forced termination (fallback):
- After cooperative cancellation fails, wait grace period (default 5 seconds)
- If still not exited, force terminate thread/coroutine (may cause resource leaks)
See PROTOCOL_SPEC §11.7.4 Timeout Enforcement
8. Memory Model¶
8.1 Object Lifecycle¶
| Object | Creation Time | Destruction Time | Lifecycle |
|---|---|---|---|
| Registry | App startup | App shutdown | App-level |
| Executor | App startup | App shutdown | App-level |
| Module instance | discover() or first call (lazy load) | unregister() or app shutdown | App-level |
| Context | Each call() | After call() returns | Request-level |
| Middleware | App startup | App shutdown | App-level |
8.2 Context.data Sharing Semantics¶
Reference Sharing Rules:
context.dataacross entire call chain is the same dict object (reference sharing)- Parent module modifications to
context.datavisible to child modules, and vice versa - When
derive()creates new Context,datafield copies reference (not deep copy)
Isolation:
- Different top-level
call()invocations use independentcontext.datainstances - Concurrently executing call chains must not share
context.data(avoid race conditions)
Example:
# Top-level call 1
context1 = Context(data={})
executor.call("module_a", {}, context1)
# module_a internally:
context.data["key"] = "value_a"
sub_context = context.derive("module_b")
executor.call("module_b", {}, sub_context)
# module_b reads "value_a" (reference sharing)
# Top-level call 2 (concurrent)
context2 = Context(data={})
executor.call("module_c", {}, context2)
# module_c's context.data is independent, doesn't contain "key"
Concurrency Safety:
- If
context.datamay be accessed by multiple threads, use thread-safe Map implementation - Python's
dictis partially thread-safe in CPython (GIL), but shouldn't rely on it
See PROTOCOL_SPEC §11.7.2 Context.data Sharing Semantics
8.3 Memory Considerations¶
context.dataaccumulates along call chain, reclaimed by GC after call completes- Avoid storing large objects (> 1MB) in
context.data, use external cache - Module instances are singleton (one instance per ID), resident in memory
- Schema objects cached after loading, not reparsed
9. Performance Characteristics¶
| Operation | Expected Latency | Description |
|---|---|---|
| Registry.get() | < 1μs | Hash table lookup |
| ACL.check() | < 100μs | Rule linear scan (< 50 rules) |
| Schema validation | < 1ms | Depends on schema complexity |
| Middleware chain | < 1ms | Depends on middleware count and complexity |
| Module execution | Depends on business | Framework overhead < 5ms |
Next Steps¶
- Module Interface Definition - Module API
- Registry API - Registry center API
- Executor API - Executor API