depa-actor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # depa-actor
2
+
3
+ `depa-actor` 是一个面向 TypeScript / JavaScript 的轻量 actor 运行时,重点支持以下几类能力:
4
+
5
+ - 基础 actor system:注册 actor、发送消息、广播消息、按邮箱优先级处理
6
+ - typed mailbox:用 `MailboxSchema` 对消息 tag 和 payload 建模
7
+ - selective receive:按 tag 检查和抽取待处理消息
8
+ - runtime 包装:把 actor 系统嵌入更高层运行时
9
+ - pipeline / dispatch 适配:方便与数据流、路由层组合
10
+ - fiber orchestration:把“状态归属”和“调度执行”拆开,支持前后台协作
11
+
12
+ 这个库适合用来构建:
13
+
14
+ - AI Agent 运行时
15
+ - 多执行单元协作系统
16
+ - 需要消息驱动 + 可测试调度语义的应用
17
+
18
+ ## 设计目标
19
+
20
+ `depa-actor` 不是为了做一个巨大的通用框架,而是聚焦几个明确目标:
21
+
22
+ - **小而清晰**:核心概念尽量少,API 保持直接
23
+ - **类型友好**:tag 和 payload 通过 schema 建模
24
+ - **消息优先**:控制语义和业务语义都优先消息化
25
+ - **可编排**:在 actor 之上支持 fiber 调度
26
+ - **可测试**:复杂行为优先通过仿真测试验证
27
+
28
+ ## 核心概念
29
+
30
+ ### 1. Actor
31
+
32
+ Actor 是状态与通信边界:
33
+
34
+ - 有唯一 id
35
+ - 有内部 state
36
+ - 通过 mailbox 收消息
37
+ - 用 handler 或 handlers 处理消息
38
+
39
+ ### 2. MailboxSchema
40
+
41
+ `MailboxSchema` 是一个 `Record<tag, payload>`:
42
+
43
+ - key:邮箱 tag
44
+ - value:该 tag 对应的 payload 类型
45
+
46
+ 示例:
47
+
48
+ ```ts
49
+ type ChatSchema = {
50
+ human_input: { text: string }
51
+ cancel: { reason: string }
52
+ tool_result: { callId: string; content: string }
53
+ }
54
+ ```
55
+
56
+ ### 3. Selective Receive
57
+
58
+ handler 内可以:
59
+
60
+ - `hasPending(tag)`:检查某类消息是否已在队列中
61
+ - `drainMailbox(tag)`:把某类消息一次性取出
62
+
63
+ 这对 AI agent 这类需要“先看控制消息,再决定是否继续”的场景非常重要。
64
+
65
+ ### 4. Fiber Orchestration
66
+
67
+ Actor 负责:
68
+
69
+ - 状态
70
+ - 身份
71
+ - mailbox
72
+
73
+ Fiber 负责:
74
+
75
+ - 调度
76
+ - 挂起
77
+ - 恢复
78
+ - 完成
79
+ - 取消
80
+
81
+ 这让复杂系统可以拆成:
82
+
83
+ - 上层业务语义
84
+ - 中层 actor 通信
85
+ - 底层 fiber 调度
86
+
87
+ 更完整的说明见:
88
+
89
+ - `ACTOR-FOR-AI-AGENTS.md`
90
+ - `doc/runtime-foundations.md`
91
+
92
+ ## 导出的模块
93
+
94
+ `src/index.ts` 当前导出以下能力:
95
+
96
+ ### Core
97
+
98
+ - 类型:
99
+ - `MailboxSchema`
100
+ - `ActorEnvelope`
101
+ - `TaggedEnvelope`
102
+ - `MailboxPriority`
103
+ - `ActorRef`
104
+ - `ActorSelf`
105
+ - `ActorHandler`
106
+ - `TagHandler`
107
+ - `ActorDef`
108
+ - `ActorLogKind`
109
+ - `ActorLogEntry`
110
+ - 实现:
111
+ - `ActorSystem`
112
+
113
+ ### Runtime
114
+
115
+ - 类型:
116
+ - `ActorPlugin`
117
+ - `CompletionWaiter`
118
+ - `SnapshotRecoveryState`
119
+ - `RuntimeSnapshotManifestBase`
120
+ - `RuntimeRootSnapshotBase`
121
+ - `ActorSnapshotBase`
122
+ - `FiberSnapshotBase`
123
+ - `SnapshotCodec`
124
+ - `RecoveryHooks`
125
+ - `PersistenceEffectPort`
126
+ - 实现:
127
+ - `ActorRuntime`
128
+ - `CompletionSignalRegistry`
129
+ - `CompletionBindingRegistry`
130
+ - `createCompletionSignalRegistry`
131
+ - `createCompletionBindingRegistry`
132
+ - `createSnapshotCodec`
133
+ - `createRecoveryHooks`
134
+ - `createPersistenceEffectPort`
135
+ - `RuntimeIndexHook`
136
+ - `createRuntimeIndexHook`
137
+
138
+ ### Pipeline
139
+
140
+ - 类型:
141
+ - `ActorPipelineDef`
142
+ - `PipelineDerivedAdapter`
143
+ - `PipelineInnerRuntimeAdapter`
144
+ - `PipelineInnerInputAdapter`
145
+ - `PipelineInnerConfigAdapter`
146
+ - `PipelineCoreLogic`
147
+ - `PipelineOutputAdapter`
148
+ - 实现:
149
+ - `createPipelineHandler`
150
+
151
+ ### Dispatch
152
+
153
+ - 类型:
154
+ - `DispatchRoute`
155
+ - 实现:
156
+ - `createDispatchHandler`
157
+
158
+ ### Orchestration
159
+
160
+ - 类型:
161
+ - `FiberId`
162
+ - `FiberStatus`
163
+ - `FiberWaitingReason`
164
+ - `SuspendPolicy`
165
+ - `SchedulerHooks`
166
+ - `FiberStep`
167
+ - `FiberRecord`
168
+ - `SpawnFiberInput`
169
+ - `DeadLetterRecord`
170
+ - `OrchestratorOptions`
171
+ - `OrchestratorState`
172
+ - `FiberAction`
173
+ - `FiberEffect`
174
+ - `ReduceResult`
175
+ - `ScheduleResult`
176
+ - 实现:
177
+ - `DEFAULT_ORCHESTRATOR_OPTIONS`
178
+ - `createOrchestratorState`
179
+ - `reduceOrchestrator`
180
+ - `applyFailure`
181
+ - `computeEffectivePriority`
182
+ - `selectNextFiberId`
183
+ - `scheduleOne`
184
+ - `createAiAgentSchedulerHooks`
185
+ - `dispatchEffects`
186
+
187
+ ## 最小示例
188
+
189
+ 下面是一个最小的 actor system 示例:
190
+
191
+ ```ts
192
+ import { ActorSystem, type ActorDef } from "@depa/actor"
193
+
194
+ type DemoSchema = {
195
+ ping: { from: string }
196
+ pong: { from: string }
197
+ }
198
+
199
+ const system = new ActorSystem<void, DemoSchema>(() => undefined)
200
+
201
+ const pingActor: ActorDef<void, DemoSchema, { count: number }> = {
202
+ initialState: { count: 0 },
203
+ handlers: {
204
+ ping(self, env) {
205
+ self.state.count += 1
206
+ self.send(env.from, "pong", { from: self.id })
207
+ },
208
+ },
209
+ }
210
+
211
+ const pongActor: ActorDef<void, DemoSchema, void> = {
212
+ initialState: undefined,
213
+ handlers: {
214
+ pong(_self, env) {
215
+ console.log("received pong from", env.payload.from)
216
+ },
217
+ },
218
+ }
219
+
220
+ system.register("alice", pingActor)
221
+ system.register("bob", pongActor)
222
+
223
+ system.sendFrom("bob", "alice", "ping", { from: "bob" })
224
+ ```
225
+
226
+ ## Selective Receive 示例
227
+
228
+ 如果某些消息优先级更高,可以配合 `priority` 和 `drainMailbox()`:
229
+
230
+ ```ts
231
+ import { ActorSystem, type ActorDef } from "@depa/actor"
232
+
233
+ type Schema = {
234
+ step: { round: number }
235
+ cancel: { reason: string }
236
+ }
237
+
238
+ const system = new ActorSystem<void, Schema>(() => undefined)
239
+
240
+ const actor: ActorDef<void, Schema, { cancelled: boolean }> = {
241
+ initialState: { cancelled: false },
242
+ priority: {
243
+ cancel: 1,
244
+ step: 100,
245
+ },
246
+ handlers: {
247
+ step(self, env) {
248
+ const pendingCancel = self.hasPending("cancel")
249
+ if (pendingCancel) {
250
+ const [cancel] = self.drainMailbox("cancel")
251
+ if (cancel) {
252
+ self.state.cancelled = true
253
+ console.log("cancelled:", cancel.payload.reason)
254
+ return
255
+ }
256
+ }
257
+
258
+ if (!self.state.cancelled) {
259
+ console.log("step", env.payload.round)
260
+ }
261
+ },
262
+ },
263
+ }
264
+
265
+ system.register("worker", actor)
266
+ ```
267
+
268
+ ## 适合什么,不适合什么
269
+
270
+ ### 适合
271
+
272
+ - 用消息驱动状态机
273
+ - 需要明确 mailbox 边界
274
+ - 需要把状态与调度分开
275
+ - 需要通过仿真测试验证复杂运行时语义
276
+ - 需要为 AI agent、多 worker、多阶段执行建模
277
+
278
+ ### 不适合
279
+
280
+ - 只想写一个简单的同步流程
281
+ - 不需要消息边界与调度语义
282
+ - 需要现成的分布式 actor 集群能力
283
+ - 需要持久化 mailbox / durable queue / exactly-once 保证
284
+
285
+ ## 测试
286
+
287
+ 运行全部测试:
288
+
289
+ ```bash
290
+ bun test
291
+ ```
292
+
293
+ 或:
294
+
295
+ ```bash
296
+ bun run test
297
+ ```
298
+
299
+ 其中有几类值得优先阅读的测试:
300
+
301
+ ### 基础 actor / scheduler
302
+
303
+ - `test/fiber-orchestrator.core.test.ts`
304
+ - `test/fiber-orchestrator.runtime.test.ts`
305
+ - `test/scheduler.generic-hooks.test.ts`
306
+
307
+ ### AI agent 相关抽象测试
308
+
309
+ - `test/ai-agent-orchestration-simulation.test.ts`
310
+ - `test/ai-agent-human-wait-policy.test.ts`
311
+ - `test/ai-agent-organization-model.test.ts`
312
+
313
+ ### 业务语义仿真测试
314
+
315
+ - `test/simulations/background-tasks/daemon_notify_queue.test.ts`
316
+ - `test/simulations/agent-teams/members_jsonl_mailboxes.test.ts`
317
+ - `test/simulations/team-protocols/protocol_fsm_shutdown_plan_approval.test.ts`
318
+ - `test/simulations/autonomous-agents/idle_cycle_auto_claim.test.ts`
319
+ - `test/simulations/cancel/interrupt_cancel_keeps_main_alive.test.ts`
320
+ - `test/simulations/shutdown/shutdown_stops_actor.test.ts`
321
+
322
+ 这些仿真测试应以当前的 primary / delegate / detached 与 member / holon + governance 模型理解。
323
+
324
+ ## 目录结构
325
+
326
+ ```text
327
+ src/
328
+ ├── core/ Actor 基础类型与 ActorSystem
329
+ ├── runtime/ ActorRuntime
330
+ ├── dispatch/ dispatch 适配层
331
+ ├── pipeline/ pipeline 适配层
332
+ └── orchestration/ fiber 调度与编排
333
+
334
+ test/
335
+ ├── simulations/ 面向业务语义的仿真测试
336
+ └── *.test.ts 核心能力测试
337
+ ```
338
+
339
+ ## 构建与开发
340
+
341
+ 安装依赖:
342
+
343
+ ```bash
344
+ bun install
345
+ ```
346
+
347
+ 构建:
348
+
349
+ ```bash
350
+ bun run build
351
+ ```
352
+
353
+ 监听编译:
354
+
355
+ ```bash
356
+ bun run dev
357
+ ```
358
+
359
+ 测试:
360
+
361
+ ```bash
362
+ bun run test
363
+ ```
364
+
365
+ ## 相关文档
366
+
367
+ - `ACTOR-FOR-AI-AGENTS.md`:如何用 actor / fiber 抽象实现 AI agent 能力
368
+ - `doc/runtime-foundations.md`:runtime foundation、边界与第一轮 adoption 说明
369
+
370
+ ## 一句话总结
371
+
372
+ `depa-actor` 的核心价值不是“把函数包装成 actor”,而是提供一套足够小、足够清晰的消息与调度抽象,使复杂系统——尤其是 AI agent 运行时——能够用可组合、可测试的方式实现。
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "depa-actor",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "dev": "tsc --watch",
21
+ "test": "bun test",
22
+ "test:watch": "bun test --watch"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.8.3"
26
+ },
27
+ "license": "MIT"
28
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * depa-actor — ActorSystem
3
+ *
4
+ * Multi-mailbox actor system with priority-based drain.
5
+ * Evolved from depa-data-graph ActorSystem with MailboxSchema support.
6
+ */
7
+
8
+ import type {
9
+ MailboxSchema,
10
+ ActorEnvelope,
11
+ ActorLogEntry,
12
+ ActorDef,
13
+ ActorRef,
14
+ ActorSelf,
15
+ TaggedEnvelope,
16
+ TagHandler,
17
+ } from './types';
18
+
19
+ // ─── ActorCell (internal) ────────────────────────────────────────────
20
+
21
+ type ActorCell<TRuntime, TSchema extends MailboxSchema, TState = void> = {
22
+ id: string;
23
+ def: ActorDef<TRuntime, TSchema, TState>;
24
+ state: TState;
25
+ queue: ActorEnvelope<TSchema>[];
26
+ processing: boolean;
27
+ };
28
+
29
+ // ─── ActorSystem ─────────────────────────────────────────────────────
30
+
31
+ export class ActorSystem<TRuntime, TSchema extends MailboxSchema> {
32
+ private seq = 0;
33
+ private cells = new Map<string, ActorCell<TRuntime, TSchema, unknown>>();
34
+
35
+ constructor(
36
+ private getRuntime: () => TRuntime,
37
+ private onLog?: (entry: ActorLogEntry<TSchema>) => void,
38
+ ) {}
39
+
40
+ // ── Query ──
41
+
42
+ ids(): string[] {
43
+ return Array.from(this.cells.keys()).sort();
44
+ }
45
+
46
+ has(id: string): boolean {
47
+ return this.cells.has(id);
48
+ }
49
+
50
+ // ── Registration ──
51
+
52
+ register<TState = void>(
53
+ id: string,
54
+ def: ActorDef<TRuntime, TSchema, TState>,
55
+ ): void {
56
+ if (this.cells.has(id)) {
57
+ throw new Error(`Actor already registered: ${id}`);
58
+ }
59
+ if (!def.handler && !def.handlers) {
60
+ throw new Error(`Actor "${id}" must have at least handler or handlers`);
61
+ }
62
+ this.cells.set(id, {
63
+ id,
64
+ def: def as ActorDef<TRuntime, TSchema, unknown>,
65
+ state: def.initialState as unknown,
66
+ queue: [],
67
+ processing: false,
68
+ });
69
+ }
70
+
71
+ unregister(id: string): void {
72
+ this.cells.delete(id);
73
+ }
74
+
75
+ // ── Messaging ──
76
+
77
+ refFrom(from: string, to: string): ActorRef<TSchema> | undefined {
78
+ if (!this.cells.has(to)) return undefined;
79
+ return {
80
+ id: to,
81
+ send: (tag, payload) => this.sendFrom(from, to, tag, payload),
82
+ };
83
+ }
84
+
85
+ sendFrom<TTag extends keyof TSchema & string>(
86
+ from: string,
87
+ to: string,
88
+ tag: TTag,
89
+ payload: TSchema[TTag],
90
+ ): void {
91
+ const target = this.cells.get(to);
92
+ const envelope: ActorEnvelope<TSchema> = {
93
+ id: ++this.seq,
94
+ ts: Date.now(),
95
+ from,
96
+ to,
97
+ tag,
98
+ payload,
99
+ };
100
+
101
+ if (!target) {
102
+ this.onLog?.({ kind: 'error', ...envelope, error: `Unknown actor: ${to}` });
103
+ return;
104
+ }
105
+
106
+ this.onLog?.({ kind: 'send', ...envelope });
107
+ target.queue.push(envelope);
108
+ this.scheduleDrain(target);
109
+ }
110
+
111
+ broadcastFrom<TTag extends keyof TSchema & string>(
112
+ from: string,
113
+ tag: TTag,
114
+ payload: TSchema[TTag],
115
+ opts?: { excludeSelf?: boolean },
116
+ ): void {
117
+ for (const id of this.cells.keys()) {
118
+ if (opts?.excludeSelf && id === from) continue;
119
+ this.sendFrom(from, id, tag, payload);
120
+ }
121
+ }
122
+
123
+ // ── Drain ──
124
+
125
+ private scheduleDrain(cell: ActorCell<TRuntime, TSchema, unknown>): void {
126
+ if (cell.processing) return;
127
+ cell.processing = true;
128
+ queueMicrotask(() => {
129
+ void this.drain(cell);
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Priority-based drain: sorts pending messages by tag priority,
135
+ * then processes sequentially. Per-tag handlers take precedence.
136
+ */
137
+ private async drain(cell: ActorCell<TRuntime, TSchema, unknown>): Promise<void> {
138
+ try {
139
+ while (cell.queue.length > 0) {
140
+ // Sort by priority (lower = higher priority, default 100)
141
+ const priorities = cell.def.priority ?? {};
142
+ cell.queue.sort((a, b) => {
143
+ const pa = (priorities as Record<string, number | undefined>)[a.tag] ?? 100;
144
+ const pb = (priorities as Record<string, number | undefined>)[b.tag] ?? 100;
145
+ return pa - pb;
146
+ });
147
+
148
+ const envelope = cell.queue.shift()!;
149
+ const self = this.makeSelf(cell);
150
+
151
+ try {
152
+ await this.dispatch(cell, self, envelope);
153
+ this.onLog?.({ kind: 'deliver', ...envelope });
154
+ } catch (err) {
155
+ const message = err instanceof Error ? err.message : String(err);
156
+ this.onLog?.({ kind: 'error', ...envelope, error: message });
157
+ }
158
+ }
159
+ } finally {
160
+ cell.processing = false;
161
+ if (cell.queue.length > 0) {
162
+ this.scheduleDrain(cell);
163
+ }
164
+ }
165
+ }
166
+
167
+ /** Route envelope to per-tag handler or unified handler */
168
+ private async dispatch(
169
+ cell: ActorCell<TRuntime, TSchema, unknown>,
170
+ self: ActorSelf<TRuntime, TSchema, unknown>,
171
+ envelope: ActorEnvelope<TSchema>,
172
+ ): Promise<void> {
173
+ const tagHandler = cell.def.handlers?.[envelope.tag] as
174
+ | TagHandler<TRuntime, TSchema, unknown, string>
175
+ | undefined;
176
+
177
+ if (tagHandler) {
178
+ await tagHandler(self, envelope as TaggedEnvelope<TSchema, string>);
179
+ } else if (cell.def.handler) {
180
+ await cell.def.handler(self, envelope);
181
+ } else {
182
+ // No handler for this tag — log and skip
183
+ this.onLog?.({
184
+ kind: 'error',
185
+ ...envelope,
186
+ error: `No handler for tag "${envelope.tag}" on actor "${cell.id}"`,
187
+ });
188
+ }
189
+ }
190
+
191
+ /** Construct ActorSelf with selective receive capabilities */
192
+ private makeSelf(
193
+ cell: ActorCell<TRuntime, TSchema, unknown>,
194
+ ): ActorSelf<TRuntime, TSchema, unknown> {
195
+ return {
196
+ id: cell.id,
197
+ ref: {
198
+ id: cell.id,
199
+ send: (tag, payload) => this.sendFrom(cell.id, cell.id, tag, payload),
200
+ },
201
+ runtime: this.getRuntime(),
202
+ state: cell.state,
203
+
204
+ send: (to, tag, payload) => this.sendFrom(cell.id, to, tag, payload),
205
+ broadcast: (tag, payload, opts) => this.broadcastFrom(cell.id, tag, payload, opts),
206
+
207
+ // Selective receive
208
+ hasPending: (tag) => cell.queue.some((e) => e.tag === tag),
209
+ drainMailbox: <TTag extends keyof TSchema & string>(tag: TTag) => {
210
+ const matching: TaggedEnvelope<TSchema, TTag>[] = [];
211
+ const remaining: ActorEnvelope<TSchema>[] = [];
212
+ for (const e of cell.queue) {
213
+ if (e.tag === tag) {
214
+ matching.push(e as TaggedEnvelope<TSchema, TTag>);
215
+ } else {
216
+ remaining.push(e);
217
+ }
218
+ }
219
+ cell.queue.length = 0;
220
+ cell.queue.push(...remaining);
221
+ return matching;
222
+ },
223
+ };
224
+ }
225
+ }