Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ Akashic 理想上的动作应该是:
│ ├─ 识别 GitHub 插件仓库
│ ├─ 执行 plugin-install
│ ├─ 检查 manifest.toml 与 plugin.py
│ └─ Runtime 自动发现并原子发布新快照
└─ 不重启,下一次执行使用新代际
│ └─ Controller 保存安装选择,更新唯一 Root 中受影响的插件
└─ 旧调用排空后释放资源,无关插件继续运行
```

安装、升级、启停、源码和 `config.local.toml` 修改都会自动热重载。正在执行的请求保持旧代际,新请求统一使用新代际;候选验证失败时继续保留旧版本。
安装、升级、启停、源码和 `config.local.toml` 修改会触发局部更新。已经接纳的调用保留实际 activation;更新停止受影响范围的新接纳并等待排空。启动或清理失败会明确保留失败状态,不能把恢复内存指针当作外部效果回滚。

想看完整机制,直接看 [插件系统 Handbook](./_handbook/plugins-tutorial.md)。
持久 binding 保存业务选择与来源证据。恢复时使用当前兼容实现;服务缺席或业务合同不兼容就失败,不重新执行归档代码。

当前合同见 [工作手册索引](./docs/INDEX.md) 和 [能力依赖与执行归属](./docs/design/issue-766-orthogonal-capabilities.md)。

---

Expand Down
15 changes: 8 additions & 7 deletions agent/host_bridge/plugin_execution.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
"""把宿主执行权限绑定到真实 Root/Context;这不是同 UID Python 沙箱。"""
from __future__ import annotations

import os
import asyncio
import os
import secrets
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

from agent.plugin_composition.context import Context
from agent.plugin_composition.execution import EXECUTION, WORKLOAD_CONTROLLER
from agent.workloads.client import WorkloadController
from agent.plugin_composition.execution import (
EXECUTION,
WORKLOAD_CONTROLLER,
ChildProcess,
PreparedProcess,
WorkloadLease,
WorkloadStartRequest,
)
from agent.workloads.client import WorkloadController
from utils.process_group import (
OwnedProcessGroup,
owned_process_env,
Expand Down Expand Up @@ -252,20 +253,20 @@ class ControllerGrant:
def __init__(self, controller: WorkloadController, owner: str, mode: Literal["candidate", "formal"], workspace_id: str):
self._controller = controller
self._owner = owner
self._mode = mode
self._mode: Literal["candidate", "formal"] = mode
self._workspace_id = workspace_id
self._identity = "resource-" + secrets.token_hex(16)

@property
def mode(self):
def mode(self) -> Literal["candidate", "formal"]:
return self._mode

@property
def workspace_id(self):
def workspace_id(self) -> str:
return self._workspace_id

@property
def identity(self):
def identity(self) -> str:
return self._identity

def _check(self, value: WorkloadStartRequest | WorkloadLease) -> None:
Expand Down
30 changes: 14 additions & 16 deletions agent/plugin_composition/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
import asyncio
import hashlib
import json
from collections.abc import AsyncIterator, Mapping
from collections.abc import AsyncIterator, Callable, Mapping
from contextlib import asynccontextmanager
from collections.abc import Callable
from typing import TYPE_CHECKING, TypeVar, cast
from typing import TYPE_CHECKING, Any, TypeVar, cast

from agent.plugin_composition.model import ServiceKey
from agent.plugin_composition.context import (
CompositionRoot,
Context,
_lifecycle_binding,
_current_runtime_scope,
_lifecycle_binding,
)
from agent.plugin_composition.model import ServiceKey
from session.log import MessageLog
from session.message_codec import json_value

Expand Down Expand Up @@ -45,7 +44,7 @@ def _expire(self) -> None:


class Bindings:
"""保存 binding 事实,并在调用者选定的 runtime scope 中打开服务。"""
"""保存业务选择与来源证据;打开当前 provider,由它校验业务兼容性。"""

def __init__(
self,
Expand All @@ -62,17 +61,17 @@ def __init__(
@property
def _log(self) -> MessageLog:
if self._storage is None:
raise RuntimeError("candidate 验证期禁止固定或打开正式 binding")
raise RuntimeError("未提供 MessageLog,不能固定或打开持久 binding")
return self._storage

def bind(
self,
service: ServiceKey[object],
service: ServiceKey[Any],
metadata: Mapping[str, object],
*,
contributors: tuple[Context, ...] = (),
) -> str:
"""从当前 OwnerCall 和真实 provider Context 固定实现。"""
"""从当前许可保存业务选择与来源归档;归档不用于恢复历史执行图。"""
log = self._log
current = _current_runtime_scope()
if current is not None:
Expand All @@ -91,10 +90,10 @@ def bind(
root = self._root
selected: set[str] = set()
pending: list[Context] = []
services: set[ServiceKey[object]] = set()
services: set[ServiceKey[Any]] = set()
contexts: dict[int, Context] = {}

def provider_for(key: ServiceKey[object], requester: Context):
def provider_for(key: ServiceKey[Any], requester: Context):
frozen = requester._fiber.dependency_store.get( # pyright: ignore[reportPrivateUsage]
key,
)
Expand All @@ -120,7 +119,7 @@ def include_context(context: Context) -> None:
selected.add(contributor)
pending.append(context)

def include_service(key: ServiceKey[object], requester: Context) -> None:
def include_service(key: ServiceKey[Any], requester: Context) -> None:
if key in services:
return
services.add(key)
Expand Down Expand Up @@ -171,15 +170,15 @@ def include_service(key: ServiceKey[object], requester: Context) -> None:
log.save_binding(identity, descriptor)
return identity

def describe(self, identity: str, service: ServiceKey[object]) -> Mapping[str, object]:
def describe(self, identity: str, service: ServiceKey[Any]) -> Mapping[str, object]:
"""只读绑定的业务选择;展示或请求投影无需启动归档目标。"""
return cast(Mapping[str, object], self._read_descriptor(identity, service)["metadata"])

@asynccontextmanager
async def open(
self, identity: str, service: ServiceKey[_T]
) -> AsyncIterator[tuple[_T, Mapping[str, object]]]:
"""在调用者已选的 Root 中打开 provider-owned 服务 scope。"""
"""在当前 Root 打开真实 provider;业务兼容性由该服务的 open 检查。"""
metadata = self.describe(identity, service)
current = _current_runtime_scope()
if current is not None and current._call._fiber.root is not self._root: # pyright: ignore[reportPrivateUsage]
Expand All @@ -189,7 +188,7 @@ async def open(
yield cast(_T, value), cast(Mapping[str, object], metadata)

def _read_descriptor(
self, identity: str, service: ServiceKey[object]
self, identity: str, service: ServiceKey[Any]
) -> Mapping[str, object]:
"""读取并校验 binding descriptor 的共同结构。"""
descriptor = self._log.read_binding(identity)
Expand All @@ -201,5 +200,4 @@ def _read_descriptor(
return descriptor



BINDINGS = ServiceKey[Bindings]("core.bindings")
23 changes: 18 additions & 5 deletions agent/plugin_composition/channel_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
from dataclasses import dataclass
from typing import Protocol

from agent.plugin_composition.model import ServiceKey
from agent.plugin_composition.channels import (
AttachmentRef, InboundEnvelope, RawInbound,
ChannelAttachmentImportPort, ChannelAttachmentReadPort,
AttachmentKind,
AttachmentReadLease,
AttachmentRef,
ChannelAttachmentImportPort,
ChannelAttachmentReadPort,
InboundEnvelope,
RawInbound,
)
from agent.plugin_composition.model import ServiceKey


class PendingInputs(Protocol):
Expand Down Expand Up @@ -44,15 +49,23 @@ class ChannelIdentity:
rollback: Callable[[object], Awaitable[bool]]


class ImportAttachment(Protocol):
async def __call__(self, data: bytes, *, kind: AttachmentKind, filename: str | None, media_type: str | None) -> AttachmentRef: ...


class AcquireAttachment(Protocol):
async def __call__(self, ref: AttachmentRef) -> AttachmentReadLease: ...


@dataclass(frozen=True, slots=True)
class ChannelAttachmentImport:
import_bytes: Callable[..., Awaitable[AttachmentRef]]
import_bytes: ImportAttachment


@dataclass(frozen=True, slots=True)
class ChannelAttachmentRead:
resolve_refs: Callable[[tuple[str, ...]], tuple[AttachmentRef, ...]]
acquire: Callable[..., object]
acquire: AcquireAttachment


INPUT_CUSTODY = ServiceKey[InputCustody]("core.input_custody")
Expand Down
5 changes: 3 additions & 2 deletions agent/plugin_composition/claims.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

from agent.plugin_composition.model import ServiceKey
from typing import Any

from agent.plugin_composition.model import ServiceKey

# Marker only: vector-backed providers declare a mutually exclusive role.
EMBEDDING_MEMORY_PLUGIN = ServiceKey[object]("plugin.claim.embedding_memory")
EMBEDDING_MEMORY_PLUGIN = ServiceKey[Any]("plugin.claim.embedding_memory")
Loading
Loading