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
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,27 @@ own services on top of the actor API.
## Agent mailbox prototype

An optional coordination layer gives each router, worker, reviewer, or subagent
its own durable actor mailbox. External agents use MCP tools and an installable
its own bounded in-memory actor mailbox. External agents use MCP tools and an installable
skill to send mail and track unread, read, processing, completed, and failed
states. No LLM runtime or coding tools are bundled.

```bash
pip install 'pulsing[mailbox]'
python -m pulsing.mailbox serve --state-dir .pulsing/agent-mail
# In the agent's stdio MCP configuration:
python -m pulsing.mailbox mcp --identity router --seed 127.0.0.1:8100
pulsing -- codex
# First machine:
SEED=10.0.0.10:8100 pulsing -- codex
# Another machine:
SEED=10.0.0.10:8100 pulsing -- codex
# The machine owning 10.0.0.10 hosts the seed; others select their address automatically.
```

This is a trusted-local-cluster prototype, not a public email service.
See the [design and setup guide](docs/src/design/agent-mailboxes.md).
The recommended [companion launcher](docs/src/design/agent-launcher.md) runs one
Pulsing process per Codex, owns separate mailboxes for its agents/subagents, and
injects MCP automatically. A [standalone Codex plugin](docs/src/design/codex-mailbox-plugin.md)
is also available.
Actor-owned event logs support offline replay, not mailbox recovery after restart.

## CLI

Expand Down
15 changes: 11 additions & 4 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,26 @@ asyncio.run(main())

## Agent 邮箱原型

可选协作层为 router、worker、reviewer、subagent 分别提供持久 actor 邮箱。
可选协作层为 router、worker、reviewer、subagent 分别提供有界内存 actor 邮箱。
外部 agent 通过 MCP 与 skill 发信、领取工作并查询未读、已读、处理中、
已完成及失败状态,不内置 LLM 运行时或 coding 工具。

```bash
pip install 'pulsing[mailbox]'
python -m pulsing.mailbox serve --state-dir .pulsing/agent-mail
# 在 agent 的 stdio MCP 配置中运行:
python -m pulsing.mailbox mcp --identity router --seed 127.0.0.1:8100
pulsing -- codex
# 多机首节点:
SEED=10.0.0.10:8100 pulsing -- codex
# 其他机器:
SEED=10.0.0.10:8100 pulsing -- codex
# 拥有 10.0.0.10 的机器自动成为 seed,其他机器自动选择本机地址加入。
```

仅用于可信本机/私有集群,不是公网邮件服务。
参考 [设计与接入文档](docs/src/design/agent-mailboxes.md)。
推荐使用 [伴生进程启动器](docs/src/design/agent-launcher.md):
每个 Codex 一个 Pulsing 进程,为其中各 agent/subagent 维护独立邮箱并自动注入 MCP。
也保留 [独立 Codex 插件接入](docs/src/design/codex-mailbox-plugin.md)。
邮箱 actor 记录事件日志供离线回放,重启不恢复信件、租约或幂等键。

## 命令行

Expand Down
183 changes: 183 additions & 0 deletions docs/src/design/agent-launcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# 用 `pulsing --` 启动 coding agent

推荐入口:

```bash
pip install 'pulsing[mailbox]'
pulsing -- codex
# Codex 的参数原样放在后面:
pulsing -- codex exec --json "检查当前仓库"
```

无需单独启动邮箱服务,也不需要先安装 marketplace 插件。这里的 `pulsing`
进程就是 Codex 的伴生节点,启动并监督一个 Codex 子进程;它同时承载 Pulsing
runtime、本实例邮箱目录及仅监听 loopback 的 MCP HTTP 接口。
不内置模型,不替代 Codex 的登录、执行权限或沙箱。

Codex 适配器使用仅限本次调用的 `-c mcp_servers...` 注入连接配置,
并用 `--no-daemon` 避免把这个受监督进程变成其他会话共享的 daemon 客户端。
协作协议通过 MCP 初始化 instructions、protocol resource 和 prompt 提供,
不会改写 `~/.codex/config.toml`、仓库规则、用户提示词或全局插件安装状态。
独立的 `pulsing` marketplace 插件是另一种接入方式,不是 wrapper 的前置条件,
也不是此处自动安装的组件。

## 每个 Codex 一个伴生进程,每个 thread 一个邮箱

```text
pulsing A
├── 本地 mailbox actors:agent / subagent 各一个
├── 本地 MCP HTTP 接口
└── Codex A
├── 主 agent
└── subagents

pulsing B
├── 本地 mailbox actors:agent / subagent 各一个
├── 本地 MCP HTTP 接口
└── Codex B
```

每个实例的目录 actor 为 `mail/instances/<instance>`。各邮箱 actor 只由所属
实例创建并持有在有界内存中;跨节点投递通过 Pulsing 命名发现直接找到收件箱。
Seed 是组网入口,不是集中持有所有邮箱的服务。

Codex 每次 MCP tool call 提供 `_meta.threadId`。伴生进程使用
`<instance>-<threadId 的稳定哈希>` 作为邮箱地址,不接受模型填写的 sender。
同一 thread 重连仍是同一个邮箱,subagent 与主 agent 共享网络而不共享身份。
缺少 metadata 的工具调用会失败,不能静默退回主 agent 的邮箱。

首次工具调用时创建邮箱。让主 agent 和每个 subagent 开始工作时先调用
`mail_identity`,再用 `mail_roles` 查找对方的地址及原始 thread ID。
不是在 Codex 的 spawn 事件发生时预建邮箱,也没有 background 模型唤醒。
该模式不提供 `mail_create_role`,子 agent 的创建仍使用 Codex 自身的工具。

## 多机组网

机器 A:

```bash
SEED=10.0.0.10:8100 pulsing --instance machine-a -- codex
```

机器 B:

```bash
SEED=10.0.0.10:8100 pulsing --instance machine-b -- codex
```

两台机器使用相同的 SEED 即可,无需指定 PULSING_ADDR:

- A 拥有 `10.0.0.10`,且端口可用:自动监听 `10.0.0.10:8100`,自身成为 seed,
不等待另一个节点先启动,也不把自身地址当作远程入口。
- B 不拥有这个 IP:向操作系统查询到该 seed 的路由,选择相应的本机接口 IP,
自动分配空闲端口,然后加入网络。多网卡、VPN 场景使用的是路由接口,
不是猜测 hostname 对应地址或固定绑定 localhost。
- 如果 seed 地址在本机且已有节点监听,后续本机实例也会选择空闲端口加入。
并发争用 seed 端口时,未抢到端口的自动实例会重新判断一次,再加入已有节点,
这个重试发生在启动目标 agent 之前,不会重复执行 agent。

也可用等价的统一参数,在 A、B 上都运行:

```bash
pulsing --seed 10.0.0.10:8100 -- codex
```

`--` 前是 Pulsing 参数,之后是目标程序及其参数,不经过 shell 重新解释。
显式 seed/addr 参数优先于环境变量。`--seed` 可重复,`SEED` 可用逗号分隔多个地址;
seed 要使用实际拥有的 IP 和固定端口(支持 IPv4 和 `[IPv6]:port`),不能是
`0.0.0.0`、`:0`、hostname 或只存在于外部 NAT 映射上的地址。
地址选择只使用本地 bind/路由查询,不发送应用探测包。
无 seed 时形成独立网络,默认监听 `127.0.0.1:0`。
加入远程 seed 时,在节点初始化完成后最多再等待 15 秒发现另一实例的目录,
发现后才启动目标程序;超时就失败,不回退创建隔离网络。建议先启动 seed 所在
机器的实例,机器之间需要双向私网连接。
如果配置地址上的监听者不是 Pulsing companion,也会因为发现失败而报错,
不会将它误认为可用集群。

高级配置仍支持 `--addr` / PULSING_ADDR 指定监听地址,优先于自动选择。
显式指定一个与本机 seed 相同的地址时,该进程作为 seed;
指定其他地址时,按该地址加入网络,不会被自动改写。
`SEED=1` 仍兼容强制 seed 模式,但不再是多机使用的推荐配置,也不能与其他地址混用。

只用于可信机器上的私网或 VPN。MCP 接口由随机 bearer token 保护,仅绑定
loopback;token 只通过子进程环境传递,不写配置文件。但 actor 集群仍不是对抗
恶意参与者的认证边界,不能把 actor 端口公开到互联网。

## 生命周期与内存边界

- 节点、空内存目录和 MCP 就绪后才启动目标程序。
- 保留目标程序的 stdin/stdout/stderr、工作目录和退出码。
- 转发终止信号,等待退出,超时后终止子进程;目标程序结束后关闭 MCP 和 actor runtime。
- 不自动重启 Codex 或重做任务;租约恢复和副作用核对仍按邮箱协议进行。
- 默认实例 ID 随每次启动生成,进程元数据和日志在
`$XDG_CACHE_HOME/pulsing/mail-runtime/agents/<instance>`(默认 `~/.cache/...`)。
- 使用稳定的 `--instance` / `PULSING_INSTANCE_ID` 可保留地址映射,但不能保留消息。
实例 ID 必须在集群内唯一,最多 24 个 ASCII 字母、数字、`_`、`-`。
- `--runtime-dir` / `PULSING_RUNTIME_DIR` 指定进程发现和日志目录;
`--state-dir` / `PULSING_STATE_DIR` 只作兼容别名。锁防止进程元数据被并发持有。
- 每个邮箱默认最多 10000 条消息、8 MiB 逻辑负载;满载拒收,不隐式丢信。

退出一个实例后,它的角色、信件、幂等键和租约消失,其他实例的邮箱不受影响。
重启不会恢复信件;不能把旧的 message ID/token 当作仍有效的任务继续提交。
没有跨机器复制、HA 或存活 MCP 会话的无缝重连。

## Actor 日志与离线回放

运行目录默认包含 `mail-events.jsonl`,由目录及邮箱 actor 自己记录:
生命周期、角色/thread 绑定、完整投递正文、状态变更、读取、领取、完成、
幂等重试、收据查询和拒绝。每个 actor 生命周期有独立 stream_id 和严格序号。
不包含 claim token;新进程从不读取日志作为状态库。

```bash
python -m pulsing.mailbox replay node-a/mail-events.jsonl node-b/mail-events.jsonl \
--format markdown --output discussion.md
```

回放只重建离线历史,不向集群投递,不恢复 live mailbox,也不调用模型。
缺失序号、损坏行或冲突重复会报错,不能悄悄跳过并声称记录完整。

## 其他 harness

`pulsing -- <任意程序> ...` 支持通用进程监督,并注入:

- `PULSING_INSTANCE_ID`:实例 ID
- `PULSING_ADDR`:本进程的实际 actor 地址
- `PULSING_MCP_URL`、`PULSING_MCP_TOKEN`:本地 MCP 连接信息
- `PULSING_PROTOCOL`:协作协议文件路径

目前只有名为 `codex` / `codex.exe` 的程序会自动获得 Codex CLI 配置。
重命名的 Codex 可用 `--adapter codex`;`--adapter generic` 禁用自动配置。
其他 harness 需要自行消费这些环境变量,并为每个 agent 提供可靠的
`_meta.threadId`;不能宣称任意程序都已自动接入 MCP。

## 可选通信审计

```bash
pulsing --audit-log .pulsing/mcp-audit.jsonl -- codex
```

也可设置 PULSING_AUDIT_LOG。每个实例建议使用独立日志文件。
审计记录经过认证的 MCP POST 请求及其响应,包括时间、实例、PID、调用关联 ID、
threadId、工具名、完整业务参数和结果、HTTP 状态、耗时与异常。
可据此追踪 send/read/claim/reply/finish 的请求、收据及处理状态。
凭据和 claim token 以 SHA-256 指纹代替,Authorization 头不写入日志。

这是额外的 MCP 边界审计,不是邮箱状态库,也不包含模型未公开的内部推理。
它不替代 Codex 的可见会话记录。日志写入失败会显露为错误;不要在磁盘不足或
日志不完整时声称已经获得完整审计。

## 验证

```bash
pytest tests/python/mailbox tests/python/cli -q
```

测试使用确定性 harness 和真实 MCP HTTP SDK,不调用模型。覆盖同进程主/子 agent
身份隔离、缺失 metadata 拒绝、跨伴生进程投递、内存所有权、重启清空、日志回放、
退出码及 SIGTERM。相同 SEED、无 PULSING_ADDR 的真实双进程测试覆盖自动引导与加入;
远程接口选择、多入口和 IPv6 通过确定性路由测试验证,不替代真实多机网络验收。
Codex CLI 0.164.0 的 `mcp list --json` 可用于不调用模型的
配置检查。完整的真实 Codex 模型/subagent 执行和多台物理机器网络仍需单独验收。

Codex thread metadata 接入依据:
[upstream MCP thread metadata](https://github.com/openai/codex/pull/18093)。
57 changes: 43 additions & 14 deletions docs/src/design/agent-mailboxes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Actor 邮箱协作原型

完整 coding-agent 接入推荐使用 [`pulsing -- codex`](agent-launcher.md):
每个 harness 伴生一个 Pulsing 进程,邮箱按本地 agent/thread 分别持有。
本文以下的固定身份 MCP/集中目录示例保留为底层原型和独立接入方式。

本原型把协作通信和模型执行分开:Pulsing 负责邮箱、消息状态和发现;
外部 agent/harness 通过 MCP 和 skill 决定如何处理任务。它不包含 LLM、
Forge、shell 工具执行器或自动拉起模型会话的逻辑。
Expand Down Expand Up @@ -37,10 +41,11 @@ flowchart TB
W["Worker agent 与自己的 MCP 进程"] --> WM
S["Subagent 与自己的 MCP 进程"] --> SM
V["Reviewer 与自己的 MCP 进程"] --> VM
WM --> DB["各邮箱独立的 SQLite 文件"]
SM --> DB
VM --> DB
RM --> DB
WM --> RAM["各 actor 独立的有界内存"]
SM --> RAM
VM --> RAM
RM --> RAM
WM -.-> LOG["只追加的 actor 事件日志,用于离线回放"]
```

目录 actor 使用 `mail/directory`;每个角色的邮箱使用
Expand Down Expand Up @@ -70,7 +75,7 @@ lease_until;没有有效 token 就不能完成。租约过期时状态仍显
创建/更新时间、状态、尝试次数和结果。收据只向发送者或收件者返回。
回复保留 thread 与原消息 ID,但不会隐式完成原任务。

投递在提交 SQLite 事务后返回;`(sender, idempotency_key)` 唯一。
投递写入所属 actor 的内存后返回;同一进程生命周期内 `(sender, idempotency_key)` 唯一。
同 key 同内容重试返回原消息,不重复投递;内容不同则拒绝。
完成操作也支持同 token、同结果的幂等重试。这里没有承诺业务副作用 exactly-once:
工具执行后 agent 崩溃,恢复者仍须先核对副作用再继续。
Expand All @@ -89,17 +94,29 @@ uv run python examples/python/mailbox_collaboration.py
输出包含四个角色、线程 ID,以及
`unread → read → processing → completed` 的发送方观察记录。

启动持久邮箱 host(Linux/macOS):
独立调试时启动内存邮箱 host(Linux/macOS):

```bash
python -m pulsing.mailbox serve \
--addr 127.0.0.1:8100 \
--state-dir .pulsing/agent-mail \
--runtime-dir .pulsing/agent-mail \
--identity router
```

同一 state-dir 只能有一个 host,进程级文件锁避免两个 host 同时拥有状态。
重启时恢复目录和所有 mailbox actor,消息、幂等键和租约记录保留。
同一 runtime-dir 只能有一个 host,文件锁只保护进程发现元数据,目录里不存邮箱数据库。
重启后角色、消息、租约和幂等键均丢失。`--state-dir` 仅保留为参数别名,不再表示持久状态。
默认的 `mail-events.jsonl` 是 actor 追加的观察日志,启动时从不读取它来恢复业务状态。

离线回放:

```bash
python -m pulsing.mailbox replay .pulsing/agent-mail/mail-events.jsonl \
--format markdown --output .pulsing/discussion.md
```

回放验证每个 actor 生命周期的 stream_id/sequence,重建信件、处理状态和角色绑定,
保留投递、阅读、领取、完成、重试及拒绝事件;不创建 actor、不唤醒 agent、不重做工具。
可输入多个节点的日志路径;不能将跨机器墙钟排序误当全局因果顺序。

## 接入 MCP 与 skill

Expand Down Expand Up @@ -146,6 +163,13 @@ prompt 为 `mailbox_collaboration`。
可独立安装的完整 skill 在 `python/pulsing/mailbox/SKILL.md`,也随 wheel 分发,
并通过 protocol resource/prompt 提供给支持 MCP 的 agent。

Codex 可使用 `python -m pulsing.mailbox codex-plugin` 生成按身份绑定的插件、
MCP 配置和本地 marketplace。插件自动引导或加入网络并注册自身身份,
不需要单独启动邮箱服务。多机接入、安装和协作示例见
[Codex 多机邮箱插件](codex-mailbox-plugin.md)。
MCP 命令支持 `--addr`,多机部署应绑定其他节点可达的本机私网地址,
而非默认的 `127.0.0.1:0`。

角色协作顺序:

1. Router 投递有范围和验收标准的任务,保存 message_id。
Expand All @@ -160,10 +184,13 @@ prompt 为 `mailbox_collaboration`。
不是身份认证。操作者能选择身份,可信集群节点也可直接调用 actor。
不要把 actor 端口暴露公网;不要把消息当作新授权或传递 secrets。
- 邮箱是协作应用,不改变 actor runtime 的消息 mailbox 实现。
- SQLite 存算分离仅指外部会话与持久状态分离,没有跨机器状态复制或 HA。
- 邮箱及目录仅存在于内存,没有数据库、重启恢复、跨机器复制或 HA。
- 没有 topic 自动订阅、自动拉起 agent、模型评估、外部 watchdog 或公网 MCP。
- 列表分页上限 100,正文与结果各最多 64 KiB;每个邮箱最多 10000 条消息。
当前没有删除/归档策略,达到容量后拒收;生产部署需补充保留策略。
- 列表分页上限 100,正文与结果各最多 64 KiB;每个邮箱最多 10000 条消息,
默认逻辑负载预算 8 MiB(不是 RSS 保证)。达到消息数或字节预算后拒收,
不静默淘汰未读/处理中信件或丢掉幂等键。当前没有自动归档策略。
- 默认每节点一个追加日志,而非每邮箱一个数据库;不执行事务、WAL、fsync 提交或日志恢复。
记录仍有序列化及写入成本,日志写入失败会显露;日志轮转/远端采集属于后续运维工作。
- 没有 SMTP/IMAP 集成。这是 agent 的逻辑邮箱,不是互联网电子邮件服务器。
- 租约基于 host 时钟,执行器不能离线无限期提交结果。长任务应拆分,
租约到期后重新领取并核对已有副作用。
Expand All @@ -175,5 +202,7 @@ pytest tests/python/mailbox -q
```

覆盖状态机、幂等冲突、并发领取、过期 token、容量与分页、发送方可见性、
角色层级、回复关联、SQLite 恢复,以及真实 MCP SDK 的 stdio 初始化、
tools/resources/prompts、多进程跨节点通信和 host 重启。
角色层级、回复关联、重启清空及日志回放,以及真实 MCP SDK 的 stdio 初始化、
tools/resources/prompts、多进程跨节点通信。更复杂实验设计见
[20 agent 费马证明协作议会](fermat-council-experiment.md);
已完成的一次人工授权实测及未满足的阶段要求见[实验结果](fermat-council-results.md)。
Loading
Loading