Skip to content

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_id corresponds 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:

  1. Cooperative cancellation (recommended):
  2. Module checks context.cancel_token.is_cancelled() and actively exits
  3. Suitable for long-running tasks (loops, I/O, etc.)

  4. Forced termination (fallback):

  5. After cooperative cancellation fails, wait grace period (default 5 seconds)
  6. 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.data across entire call chain is the same dict object (reference sharing)
  • Parent module modifications to context.data visible to child modules, and vice versa
  • When derive() creates new Context, data field copies reference (not deep copy)

Isolation:

  • Different top-level call() invocations use independent context.data instances
  • 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.data may be accessed by multiple threads, use thread-safe Map implementation
  • Python's dict is 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.data accumulates 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