Cordis Python 版:介绍与实现思路

基于论文《A Programming Paradigm for Spatiotemporal Composability》与 Node.js 版 Cordis 的移植设计

项目名:cordis-py · 环境:Python 3.12 + uv

1. 论文要点:为什么需要 Cordis

论文提出现代软件在“动态组合”上缺少形式化基础,并把动态组合拆成两个正交维度:

静态场景下,这两个维度分别对应 RAII / 词法作用域和模块导入解析;在动态场景下,组件会在运行期到达或离开,因此需要运行期机制来支撑可逆副作用和响应式依赖。

核心机制

论文概念含义
revertible effects所有通过 Context 的修改都经由 ctx.effect(callback) 登记;callback 返回逆操作,卸载时按 LIFO 顺序执行,实现“效果可逆”。
reactive coeffects组件用 inject 声明依赖;依赖对应“coeffect”,其出现、消失或身份变化会触发依赖方自动激活/停用。
unified context效果上下文和余效果上下文统一为同一个 Context,通过它中介所有读写,从而获得观察等价性。
component / fiber组件是可声明依赖、可执行效果函数的代码单元;fiber 是组件的一次运行实例,有完整生命周期。

实现分层

  1. 核心库:实现 effect tracking、coeffect resolution、fiber 生命周期、Context 访问。
  2. 组件加载器:把声明式配置(YAML/JSON 等)落实为 fiber 树,支持增量协调。
  3. 应用框架:在 Cordis 之上构建具体领域框架(论文中案例为 Koishi)。
Python 版定位:不是重写一个具体应用框架,而是实现“元框架”核心——提供通用的动态组合语义,让上层应用或 Agent Harness 可以像使用 Node 版 Cordis 一样使用它。

2. Node Cordis 包参考

Node 版 Cordis(v4)已开源并作为 DeepSeek Harness 的底层插件框架。它是我们 Python 移植的主要 API 与语义参照。

2.1 快速示例(TypeScript 惯例)

const root = new Context()

class Counter extends Service {
  value = 0
  constructor(ctx: Context) {
    super(ctx, 'counter')
  }
  next() { return ++this.value }
}

const greeter = Object.assign((ctx: Context) => {
  ctx.on('app/ready', (msg) => console.log(ctx.counter.next(), msg))
}, { inject: ['counter'] })

await root.plugin(Counter)
await root.plugin(greeter)
root.emit('app/ready', 'started')
await root.fiber.dispose()

2.2 核心对象

对象/API作用
Context根依赖容器,也是插件的运行上下文;通过 Proxy 支持 ctx.serviceName 属性访问。
ctx.plugin()装载一个插件,返回 Fiber;Fiber 可 await,等待启动完成。
ctx.inject()声明依赖后运行函数式插件;依赖不满足时等待,满足后自动生命周期管理。
ctx.effect()登记可逆副作用;支持函数返回 disposer、同步/异步 iterable 等形态。
ctx.provide()注册服务,自动在 fiber 卸载时注销并通知依赖方。
ctx.get() / ctx.set()反射式读写 coeffect 值(不强制通过 inject 声明)。
ctx.isolate() / ctx.intercept()创建子 Context:隔离某个服务的绑定域,或为服务访问追加拦截/配置元数据。
ctx.on() / ctx.emit()类型化事件总线,监听器随 fiber 自动注销。
ctx.parallel / serial / bail / waterfall不同事件分发模式:并行、串行、短路、中间件式瀑布。
Fiber组件实例,状态机:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED,以及 FAILED。
Service基类;子类构造时调用 super(ctx, name) 即自动注册为服务。
RegistryService插件规范化、运行时记录、fiber 集合管理。
ReflectService支撑 Context Proxy:属性解析、provide/get/set、accessor、mixin。

2.3 加载器与 HMR

Node 生态中,@cordisjs/plugin-loader、@cordisjs/plugin-include、@cordisjs/plugin-group、@cordisjs/plugin-hmr 提供声明式配置和热模块替换。Python 版应保留这些概念:

3. Python 版总体设计

3.1 设计目标

3.2 包结构

cordis_py/
├── pyproject.toml
├── README.md
├── LICENSE
├── docs/
│   └── cordis_py_intro.html        # 本文档
├── src/
│   └── cordis_py/
│       ├── __init__.py             # 公共导出
│       ├── context.py              # Context 根/子上下文、属性解析
│       ├── fiber.py                # Fiber、FiberState、effect 执行与回收
│       ├── events.py               # EventsService:emit/parallel/serial/bail/waterfall
│       ├── registry.py             # Plugin 规范化、RegistryService
│       ├── reflect.py              # 服务注册表、get/set/provide/accessor/mixin
│       ├── service.py              # Service 基类、@inject 装饰器
│       ├── loader.py               # Entry、Loader、Group、Include
│       ├── hmr.py                  # 热模块替换
│       ├── config.py               # YAML/JSON 配置解析与校验(可选依赖)
│       └── utils.py                # DisposableList、错误码、公共工具
├── tests/
│   ├── test_effects.py
│   ├── test_fiber_lifecycle.py
│   ├── test_events.py
│   ├── test_services.py
│   ├── test_loader.py
│   └── test_hmr.py
└── .python-version                 # 3.12

3.3 核心抽象

Fiber:组件运行实例

class FiberState(enum.Enum):
    PENDING = "pending"      # 等待依赖
    LOADING = "loading"      # 正在执行组件
    ACTIVE = "active"        # 已激活、正常提供
    FAILED = "failed"        # 启动/校验失败
    UNLOADING = "unloading"  # 正在回滚
    DISPOSED = "disposed"    # 已彻底移除

@dataclass
class Fiber:
    uid: int
    parent: Context
    inject: dict[str, Any]
    config: Any
    state: FiberState
    effects: list[Disposable]          # LIFO 逆操作账本
    committed: dict[str, Provider]     # 激活时依赖视图
    target: str | None                 # 当前目标状态
    inertia: asyncio.Task | None       # 在途 transition
    store: dict[str, Impl] | None      # 本 fiber 提供的服务

效果(Effect)

论文中 ctx.effect 接受回调,回调返回:

Python 实现可借助 inspect.isgenerator / inspect.isasyncgen,将每个 disposer 压入当前 fiber 的 effect 栈。卸载时倒序执行并 await 异步 disposer。

async def execute(callback, guard):
    iterable = callback()
    disposers = []
    if inspect.isasyncgen(iterable):
        async for d in iterable:
            if callable(d):
                disposers.append(d)
    elif inspect.isgenerator(iterable):
        for d in iterable:
            if callable(d):
                disposers.append(d)
    elif callable(iterable):
        disposers.append(iterable)
    # 若需要支持“边执行边允许中断”,则按论文 Algorithm 1 在每步检查 guard
    return disposers  # 由调用方组合为单个逆操作

Context:服务与依赖的中介

class Context:
    fiber: Fiber
    parent: Context | None
    _isolate: dict[str, object]       # 服务名 -> realm
    _intercept: dict[str, Any]        # 服务名 -> 拦截/配置元数据

    # 核心效果原语
    def effect(self, callback: EffectCallback) -> Disposable: ...
    # 依赖相关
    def plugin(self, plugin: Plugin, config: Any = None) -> Fiber: ...
    def inject(self, deps: Inject, callback: Callable) -> Fiber: ...
    # coeffect
    def get(self, name: str, strict: bool = True) -> Any: ...
    def set(self, name: str, value: Any) -> None: ...
    def provide(self, name: str, value: Any) -> Disposable: ...
    # 作用域
    def isolate(self, name: str, realm: object | None = None) -> Context: ...
    def intercept(self, name: str, metadata: Any) -> Context: ...
    # 事件
    def on(self, event: str, handler, *, prepend=False) -> Disposable: ...
    def emit(self, event: str, *args) -> None: ...
    async def parallel(self, event: str, *args) -> None: ...
    async def serial(self, event: str, *args) -> Any: ...
    def bail(self, event: str, *args) -> Any: ...
    def waterfall(self, event: str, *args, next_fn=None) -> Any: ...

属性访问与注入校验

Python 没有 JavaScript Proxy,但可用 __getattr__ 实现类似语义:当插件访问 ctx.model 时,从当前 fiber 的 committed 视图向上查找;若该名称在当前 fiber 的 inject 中但尚未 committed,则抛“依赖未激活”;若一直到根都没有声明,则抛“未声明访问”。这与论文 Algorithm 6 的语义一致。

def __getattr__(self, name: str):
    if name.startswith("_"):
        raise AttributeError(name)
    fiber = self.fiber
    while True:
        if name in fiber.committed:
            return fiber.committed[name]
        if name in fiber.inject:
            raise InactiveAccess(name)
        if fiber.parent is None:
            raise UndeclaredAccess(name)
        fiber = fiber.parent.fiber

插件形态

# 函数式
def my_plugin(ctx: Context, config: dict) -> Disposable | None:
    ctx.provide("foo", Foo())
    return lambda: cleanup()

my_plugin.inject = ["foo"]

# 类式
class MyPlugin:
    inject = ["foo"]
    def __init__(self, ctx: Context, config: dict):
        self.ctx = ctx
        ctx.provide("bar", Bar())

# 对象式
class MyObjectPlugin:
    def apply(self, ctx: Context, config: dict): ...

事件分发

方法语义Python 注意点
emit同步广播,不等待异步监听器同步广播即可;异步监听器可 asyncio.ensure_future 调度。
parallel并发执行并等待全部完成用 asyncio.gather(..., return_exceptions=True) 或 asyncio.TaskGroup。
serial按注册顺序逐一 await,遇 bail 值短路把非 None/False 返回值视为短路值。
bail同步短路,返回第一个非空结果适合“找到第一个能处理者”的场景。
waterfall中间件链监听器接收 (*args, next);不调用 next() 即短路。

3.4 实现依据与边界

本实现不参考任何现有的 Python Cordis 包。

设计参考仅包括:

因此本项目不存在需要兼容的历史 Python 实现;API 以 Node 版 Cordis 为参照,并结合 Python 异步生态做了必要调整。

4. 核心 API 草案与示例

4.1 最小可运行示例

import asyncio
from cordis_py import Context, Service, inject


class Greeter(Service):
    def __init__(self, ctx: Context):
        super().__init__(ctx, "greeter")

    def hello(self, name: str) -> str:
        return f"Hello, {name}!"


@inject("greeter")
def greeter_plugin(ctx: Context, config: dict):
    def on_ready(msg: str):
        print(ctx.greeter.hello(msg))

    ctx.on("app/ready", on_ready)
    return lambda: print("greeter_plugin disposed")


async def main():
    root = Context()
    await root.plugin(Greeter)
    await root.plugin(greeter_plugin)

    root.emit("app/ready", "Cordis Python")
    await root.fiber.dispose()


asyncio.run(main())

4.2 生命周期与依赖响应

async def main():
    root = Context()

    # 先加载消费者,依赖未满足,进入 PENDING
    consumer = root.plugin(consumer_plugin)   # inject = ["model"]

    # 加载提供者后,消费者自动从 PENDING -> LOADING -> ACTIVE
    provider = root.plugin(model_plugin, {"provider": "deepseek"})
    await provider.await()
    await consumer.await()

    # 卸载提供者,依赖方应自动回滚并进入 PENDING 或 UNLOADING
    await provider.dispose()

    # 重新提供,消费者恢复
    provider2 = root.plugin(model_plugin, {"provider": "openai"})
    await provider2.await()
    await consumer.await()

    await root.fiber.dispose()

4.3 声明式 Loader 配置草案

# cordis.yml
plugins:
  - id: greeter
    url: "myapp.plugins:Greeter"
    config:
      language: "zh"
  - id: storage
    url: "myapp.plugins:Storage"
    config:
      backend: "sqlite"
    disabled: false
  - id: web
    url: "myapp.plugins:Web"
    isolate:
      db: true          # 本地 realm:随条目移动
    intercept:
      logger:
        level: "debug"
    config:
      port: 8080
from cordis_py.loader import Loader

loader = Loader(root)
await loader.include("cordis.yml")
# 修改配置后调用 reconcile,仅重建发生变化的条目
await loader.reconcile()

4.4 热模块替换(HMR)

  1. 模块分类:对变更模块集合计算 accepted/declined 集合(模块导入边由运行时追踪器 + AST 静态补全两路构成)。
  2. 过期条目检测:找出 import 依赖树中触及 accepted 模块的 Loader 条目。
  3. 事务式重载:快照旧插件与模块命名空间,dispose 旧 fiber,按拓扑序重载变更模块与条目模块,再重新 apply;任一步失败则回滚并重抛。
from cordis_py import HMR

hmr = HMR(loader)
await hmr.reload_file("src/plugins/worker.py")   # 变更文件 → 自动分类并事务式重载
await hmr.reload_module("myapp.helpers")         # 或按模块名手动触发
hmr.dispose()                                    # 退出时卸载依赖图追踪器

5. 实现路线

阶段内容验收标准
Phase 0初始化 uv 工程、src 布局、py.typed、pytest、基本导出uv run pytest 通过,import cordis_py 可用。
Phase 1Context + effect 账本 + Disposable 组合同步/异步 disposer、生成器效果可倒序回收。
Phase 2Fiber 生命周期、依赖注入、服务注册与通知先装消费者后装提供者能自动启动;卸载提供者触发依赖方回滚。
Phase 3事件系统:on/once、四种异步模式和 waterfall监听器随 fiber 自动清理;模式语义测试通过。
Phase 4Service 基类、isolate/intercept、accessor/mixin、访问控制多 realm 服务互不干扰;未声明访问抛错。
Phase 5声明式 Loader:Entry、Group、Include、YAML/JSON、reconcile增删改配置只影响相关 fiber,且最终状态等于全新加载。
Phase 6HMR:文件监听、模块分类、过期检测、事务重载修改组件源码后无需重启进程,旧副作用被清理。
Phase 7文档、日志、计时器等扩展,性能与并发压力测试提供完整 README 和 API 参考。
建议首版范围:先完成 Phase 0-4 作为 cordis-py 核心包;Phase 5-6 可作为同一包内的可选子模块或独立扩展包,避免核心依赖过重。

TODO / 实现状态

此节用于记录当前 Python 实现已经完成和尚未完成的功能点,避免后续迭代时遗忘。

已完成

待办 / 后续功能点

实现依据说明:本项目仅参考论文《A Programming Paradigm for Spatiotemporal Composability》与 Node.js 版 Cordis(Cordis v4 / DeepSeek Harness vendor)进行设计,不参考任何现有的 Python Cordis 实现包。

6. 打包与工程化

仓库已使用 uv 和 Python 3.12。建议按以下方式组织 pyproject.toml:

[project]
name = "cordis-py"
version = "0.1.0"
description = "A Python implementation of Cordis: spatiotemporal composability for dynamic systems"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
dependencies = [
  # 核心无第三方依赖;可选扩展按 extra 引入
]

[project.optional-dependencies]
yaml = ["pyyaml>=6.0"]
hmr = ["watchfiles>=0.21"]
dev = ["pytest>=8", "pytest-asyncio>=0.23", "ruff>=0.6", "mypy>=1.10"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/cordis_py"]

常用命令:

# 安装开发环境
uv sync --extra dev

# 运行测试
uv run pytest

# 构建 wheel / sdist
uv build

# 本地安装为包
uv pip install -e .

7. 风险、取舍与待确认问题