orbit-agent-runtime 0.8.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +750 -0
  2. package/LICENSE +201 -0
  3. package/README.md +492 -0
  4. package/README.zh-CN.md +389 -0
  5. package/benchmarks/README.md +24 -0
  6. package/benchmarks/gateway.mjs +68 -0
  7. package/benchmarks/pae.mjs +90 -0
  8. package/benchmarks/replay.mjs +51 -0
  9. package/benchmarks/run-all.mjs +24 -0
  10. package/benchmarks/wal.mjs +50 -0
  11. package/bin/orbit.mjs +523 -0
  12. package/dist/.tsbuildinfo +1 -0
  13. package/dist/demo-deepseek.d.ts +2 -0
  14. package/dist/demo-deepseek.d.ts.map +1 -0
  15. package/dist/demo-deepseek.js +66 -0
  16. package/dist/demo-deepseek.js.map +1 -0
  17. package/dist/demo-host.d.ts +2 -0
  18. package/dist/demo-host.d.ts.map +1 -0
  19. package/dist/demo-host.js +98 -0
  20. package/dist/demo-host.js.map +1 -0
  21. package/dist/demo-replay.d.ts +2 -0
  22. package/dist/demo-replay.d.ts.map +1 -0
  23. package/dist/demo-replay.js +68 -0
  24. package/dist/demo-replay.js.map +1 -0
  25. package/dist/src/core/orbitRuntimeHost.d.ts +349 -0
  26. package/dist/src/core/orbitRuntimeHost.d.ts.map +1 -0
  27. package/dist/src/core/orbitRuntimeHost.js +706 -0
  28. package/dist/src/core/orbitRuntimeHost.js.map +1 -0
  29. package/dist/src/index.d.ts +92 -0
  30. package/dist/src/index.d.ts.map +1 -0
  31. package/dist/src/index.js +226 -0
  32. package/dist/src/index.js.map +1 -0
  33. package/dist/test/audit_chain.test.d.ts +2 -0
  34. package/dist/test/audit_chain.test.d.ts.map +1 -0
  35. package/dist/test/audit_chain.test.js +283 -0
  36. package/dist/test/audit_chain.test.js.map +1 -0
  37. package/dist/test/channel_hub.test.d.ts +2 -0
  38. package/dist/test/channel_hub.test.d.ts.map +1 -0
  39. package/dist/test/channel_hub.test.js +66 -0
  40. package/dist/test/channel_hub.test.js.map +1 -0
  41. package/dist/test/cordis_adapter.test.d.ts +2 -0
  42. package/dist/test/cordis_adapter.test.d.ts.map +1 -0
  43. package/dist/test/cordis_adapter.test.js +280 -0
  44. package/dist/test/cordis_adapter.test.js.map +1 -0
  45. package/dist/test/cost_routing.test.d.ts +2 -0
  46. package/dist/test/cost_routing.test.d.ts.map +1 -0
  47. package/dist/test/cost_routing.test.js +31 -0
  48. package/dist/test/cost_routing.test.js.map +1 -0
  49. package/dist/test/domain_transaction.test.d.ts +2 -0
  50. package/dist/test/domain_transaction.test.d.ts.map +1 -0
  51. package/dist/test/domain_transaction.test.js +351 -0
  52. package/dist/test/domain_transaction.test.js.map +1 -0
  53. package/dist/test/file_channel.test.d.ts +2 -0
  54. package/dist/test/file_channel.test.d.ts.map +1 -0
  55. package/dist/test/file_channel.test.js +179 -0
  56. package/dist/test/file_channel.test.js.map +1 -0
  57. package/dist/test/gateway.test.d.ts +2 -0
  58. package/dist/test/gateway.test.d.ts.map +1 -0
  59. package/dist/test/gateway.test.js +548 -0
  60. package/dist/test/gateway.test.js.map +1 -0
  61. package/dist/test/governance_profile.test.d.ts +2 -0
  62. package/dist/test/governance_profile.test.d.ts.map +1 -0
  63. package/dist/test/governance_profile.test.js +214 -0
  64. package/dist/test/governance_profile.test.js.map +1 -0
  65. package/dist/test/governance_schema.test.d.ts +2 -0
  66. package/dist/test/governance_schema.test.d.ts.map +1 -0
  67. package/dist/test/governance_schema.test.js +200 -0
  68. package/dist/test/governance_schema.test.js.map +1 -0
  69. package/dist/test/host_integration.test.d.ts +2 -0
  70. package/dist/test/host_integration.test.d.ts.map +1 -0
  71. package/dist/test/host_integration.test.js +104 -0
  72. package/dist/test/host_integration.test.js.map +1 -0
  73. package/dist/test/host_journal_persistence.test.d.ts +2 -0
  74. package/dist/test/host_journal_persistence.test.d.ts.map +1 -0
  75. package/dist/test/host_journal_persistence.test.js +367 -0
  76. package/dist/test/host_journal_persistence.test.js.map +1 -0
  77. package/dist/test/impact_domain.test.d.ts +2 -0
  78. package/dist/test/impact_domain.test.d.ts.map +1 -0
  79. package/dist/test/impact_domain.test.js +48 -0
  80. package/dist/test/impact_domain.test.js.map +1 -0
  81. package/dist/test/injected_clock.test.d.ts +13 -0
  82. package/dist/test/injected_clock.test.d.ts.map +1 -0
  83. package/dist/test/injected_clock.test.js +200 -0
  84. package/dist/test/injected_clock.test.js.map +1 -0
  85. package/dist/test/isolation_domain.test.d.ts +2 -0
  86. package/dist/test/isolation_domain.test.d.ts.map +1 -0
  87. package/dist/test/isolation_domain.test.js +302 -0
  88. package/dist/test/isolation_domain.test.js.map +1 -0
  89. package/dist/test/journal_wal.test.d.ts +2 -0
  90. package/dist/test/journal_wal.test.d.ts.map +1 -0
  91. package/dist/test/journal_wal.test.js +622 -0
  92. package/dist/test/journal_wal.test.js.map +1 -0
  93. package/dist/test/mcp_adapter.test.d.ts +2 -0
  94. package/dist/test/mcp_adapter.test.d.ts.map +1 -0
  95. package/dist/test/mcp_adapter.test.js +440 -0
  96. package/dist/test/mcp_adapter.test.js.map +1 -0
  97. package/dist/test/openai_compat_channel.test.d.ts +2 -0
  98. package/dist/test/openai_compat_channel.test.d.ts.map +1 -0
  99. package/dist/test/openai_compat_channel.test.js +325 -0
  100. package/dist/test/openai_compat_channel.test.js.map +1 -0
  101. package/dist/test/openapi_adapter.test.d.ts +2 -0
  102. package/dist/test/openapi_adapter.test.d.ts.map +1 -0
  103. package/dist/test/openapi_adapter.test.js +270 -0
  104. package/dist/test/openapi_adapter.test.js.map +1 -0
  105. package/dist/test/orbit_cli.test.d.ts +2 -0
  106. package/dist/test/orbit_cli.test.d.ts.map +1 -0
  107. package/dist/test/orbit_cli.test.js +137 -0
  108. package/dist/test/orbit_cli.test.js.map +1 -0
  109. package/dist/test/pact_verifier.test.d.ts +2 -0
  110. package/dist/test/pact_verifier.test.d.ts.map +1 -0
  111. package/dist/test/pact_verifier.test.js +41 -0
  112. package/dist/test/pact_verifier.test.js.map +1 -0
  113. package/dist/test/pae_adapter.test.d.ts +2 -0
  114. package/dist/test/pae_adapter.test.d.ts.map +1 -0
  115. package/dist/test/pae_adapter.test.js +469 -0
  116. package/dist/test/pae_adapter.test.js.map +1 -0
  117. package/dist/test/plugin_sandbox_guard.test.d.ts +2 -0
  118. package/dist/test/plugin_sandbox_guard.test.d.ts.map +1 -0
  119. package/dist/test/plugin_sandbox_guard.test.js +57 -0
  120. package/dist/test/plugin_sandbox_guard.test.js.map +1 -0
  121. package/dist/test/rate_limit_behavior.test.d.ts +2 -0
  122. package/dist/test/rate_limit_behavior.test.d.ts.map +1 -0
  123. package/dist/test/rate_limit_behavior.test.js +85 -0
  124. package/dist/test/rate_limit_behavior.test.js.map +1 -0
  125. package/dist/test/replay_compat.test.d.ts +2 -0
  126. package/dist/test/replay_compat.test.d.ts.map +1 -0
  127. package/dist/test/replay_compat.test.js +1122 -0
  128. package/dist/test/replay_compat.test.js.map +1 -0
  129. package/dist/test/replay_engine.test.d.ts +2 -0
  130. package/dist/test/replay_engine.test.d.ts.map +1 -0
  131. package/dist/test/replay_engine.test.js +85 -0
  132. package/dist/test/replay_engine.test.js.map +1 -0
  133. package/dist/test/shell_channel.test.d.ts +2 -0
  134. package/dist/test/shell_channel.test.d.ts.map +1 -0
  135. package/dist/test/shell_channel.test.js +173 -0
  136. package/dist/test/shell_channel.test.js.map +1 -0
  137. package/dist/test/teardown_isolation.test.d.ts +10 -0
  138. package/dist/test/teardown_isolation.test.d.ts.map +1 -0
  139. package/dist/test/teardown_isolation.test.js +279 -0
  140. package/dist/test/teardown_isolation.test.js.map +1 -0
  141. package/dist/test/token_budget.test.d.ts +2 -0
  142. package/dist/test/token_budget.test.d.ts.map +1 -0
  143. package/dist/test/token_budget.test.js +113 -0
  144. package/dist/test/token_budget.test.js.map +1 -0
  145. package/dist/test/trace_journal.test.d.ts +2 -0
  146. package/dist/test/trace_journal.test.d.ts.map +1 -0
  147. package/dist/test/trace_journal.test.js +43 -0
  148. package/dist/test/trace_journal.test.js.map +1 -0
  149. package/dist/test/trace_journal_persistence.test.d.ts +2 -0
  150. package/dist/test/trace_journal_persistence.test.d.ts.map +1 -0
  151. package/dist/test/trace_journal_persistence.test.js +175 -0
  152. package/dist/test/trace_journal_persistence.test.js.map +1 -0
  153. package/dist/test/trace_persistence.test.d.ts +2 -0
  154. package/dist/test/trace_persistence.test.d.ts.map +1 -0
  155. package/dist/test/trace_persistence.test.js +216 -0
  156. package/dist/test/trace_persistence.test.js.map +1 -0
  157. package/dist/test/trip_protector.test.d.ts +2 -0
  158. package/dist/test/trip_protector.test.d.ts.map +1 -0
  159. package/dist/test/trip_protector.test.js +42 -0
  160. package/dist/test/trip_protector.test.js.map +1 -0
  161. package/examples/README.md +34 -0
  162. package/examples/cli-record-replay.mjs +74 -0
  163. package/examples/custom-channel.mjs +102 -0
  164. package/examples/js-pae-plugin.mjs +128 -0
  165. package/examples/mcp-adapter.mjs +102 -0
  166. package/package.json +60 -0
  167. package/src/core/orbitRuntimeHost.ts +853 -0
  168. package/src/index.ts +256 -0
@@ -0,0 +1,389 @@
1
+ # Orbit Agent Runtime
2
+
3
+ > **Deterministic · Provable · Governable**
4
+ > 面向插件化智能体的轻量运行内核:插件热注册、可证明的故障隔离、全链路轨迹溯源、沙箱化执行。
5
+
6
+ [English](./README.md) · **简体中文**
7
+
8
+ **项目状态:** `pre-alpha` · **许可证:** Apache-2.0 · **轨道:** 开源产品(见[路线图](#设计路线图))
9
+
10
+ > Orbit 以**开源产品**(而非实验项目)的轨道推进:内核是产品级工程——strict TypeScript、
11
+ > 零运行时依赖、每个机制都有测试与架构宪章([docs/VISION.md](./docs/VISION.md))背书。
12
+ > 当前阶段:内核机制全部完成(M1–M4)→ 产品化攻坚(真实模型适配、持久化、CLI、npm 发布)。
13
+ > 欢迎按 [CONTRIBUTING.md](./CONTRIBUTING.md) 参与贡献。
14
+
15
+ Orbit Agent Runtime 是一套零第三方依赖的插件化智能体运行时宿主。所有外部能力(模型访问、存储、IO)统一抽象为**能力通道(Channel)**;智能体不直接调用任何能力,一切调用经由通道集线器调度。内核分层严格单向依赖,内部状态私有、对外只读副本。
16
+
17
+ ## 核心特性
18
+
19
+ - **通道优先解耦** —— 内存 KV、LLM 等能力全部通道化;插件可运行时覆盖或扩展通道(插件通道优先,内置通道兜底)
20
+ - **插件规约校验** —— 必填字段完整性、宿主版本兼容、能力声明三重校验,非法插件注册即拦截
21
+ - **跳闸保护** —— 插件级故障状态机(正常 → 跳闸 → 探测),单插件故障永不击穿宿主
22
+ - **轨迹日志本** —— 全链路行为记录,支持快照保存与恢复,可审计、可复盘
23
+ - **沙箱池** —— 每智能体独立沙箱,循环上限防死循环,每轮运行独立追踪 ID
24
+ - **确定性重放(M2)** —— 记录一次运行后零模型调用精确重放,输出逐字节一致,digest 链对账校验
25
+ - **可证明隔离(M3)** —— 插件/通道/沙箱依赖建模为有向图,故障影响 = 反向可达闭包,附隔离定理
26
+ - **成本感知路由(M4)** —— 通道声明成本/延迟/质量,智能体按每轮预算调度
27
+ - **统一网关入口(W7)** —— `capabilityInvoke` 即确定性边界:每次调用的治理决策(熔断 / 契约 / 预算 / 限流 / 路由 / 压缩)被记录并在重放时还原,配置漂移与 digest 漂移被分别报告
28
+ - **Token 预算与压缩(W8)** —— `TokenBudgetEngine` 是纯函数(禁随机/禁时钟)的 token 估算器与确定性头截压缩器;预算/路由决策改由引擎与通道注册表计算,其阈值配置哈希进运行指纹以支持漂移检测
29
+ - **负载感知的存储压缩(W9)** —— 大体积记录输出经 `packSnapshot` 在落盘时透明 `deflate` 压缩(零外部依赖),而消费者始终拿到原始值、重放逐字节一致;`compression` 决策(`level` / `applied` / `bytesSaved`)被记录以供审计
30
+ - **限流与行为采集(W11)** —— `RateLimiter` 是纯函数(禁随机/禁时钟)的调用计数预算;`rateLimited` 决策被记录并在重放时逐字还原(重放旁路限流器)。`BehaviorCollector` 以三模式采集结构化 `BehaviorNote`——`record`(随轨迹落盘)/ `live`(提案,不落盘)/ `replay`(旁路)
31
+ - **三分漂移分类(W13)** —— 重放失败被区分为明确错误:配置漂移(`RunFingerprintDriftError`,版本/指纹)、决策漂移(`DecisionDriftError`,如契约被撤销)、调用漂移(`ReplayDriftError`,数据/签名);对账另报 `decisionDriftFields`
32
+ - **`replay_compat` 确定性门禁(W12)** —— 27 例 CI 门禁证明网关边界在压缩/限流/采集/指纹漂移/决策漂移/PAE 适配器/持久化窗口下始终忠实:每个决策被记录、并重放逐字节一致
33
+ - **插件适配引擎 PAE(W15)** —— 外来运行时(进程内 JS,MCP / OpenAPI / Cordis)经适配器映射为内核能力契约,整体发布为单一能力通道;每次外来调用都是一笔网关事务,被记录并可逐字节重放。保真度诚实协商(`full | reduced | lossy`),适配面哈希进运行指纹以支持漂移检测
34
+ - **隔离域(W19)** —— 影响域图驱动物理层分配:故障闭包超阈值的单元获得独立 L2 子进程(`iso:<unit>`),其余确定性分块共置(`shared:<n>`)。同步是 diff 而非重建,域整体发布为单一能力通道,域调用被记录并可逐字节重放
35
+ - **跨域事务(W20)** —— 域间每一次跳转都是一笔原子网关事务:决策(归属/隔离级)+ 执行 + 结果 + 审计,按(源域→目标域)对账结算;孤儿(跨界未结算)与拒绝(执行前被拒)都能从记录单独检出;重放注入冻结输出而不重入域
36
+ - **日志持久化(W27)** —— 审计日志与录制窗口各挂一份崩溃安全的预写日志(WAL),进程重启不再擦除审计轨迹与已录制运行。一行一条 JSON,故崩溃唯一残留形态是「末行被截断」:恢复只丢弃那一行,而任何**内部**非法行按真实故障拒绝。恢复保留原始 id 与顺序,因此条目逐字节一致——被进程边界切开的录制窗口仍重放为一条连续运行
37
+ - **信任推定与契约化(W31)** —— VISION §3.1 最后两维落地:`strict` 把外来适配器封顶 L1(不跑子进程)并要求每个插件声明参数契约(`schema`);`standard` 在调用前按声明校验参数;`sandbox` 零校验。四档模式全部维度都有代码实现
38
+ - **审计哈希链(W30)** —— append-only 审计日志的可信度只有文件权限那么高;哈希链让它可证明未被篡改。`new OrbitRuntimeHost({ auditSigningKey })` 后每条审计条目携带 HMAC-SHA256 的 `prevHash`/`chainHash` 链,`host.verifyAuditChain()` 证明完整性,`orbit audit <trace.wal> --key …` 从 CLI 验证,`strict` 档在链被篡改时拒绝启动。改动任何一条都会在该处及之后全部断裂
39
+ - **四档治理模式(W29)** —— VISION 的 Sandbox / Standard / Strict 档位从"设计目标"变为可切换配置:`new OrbitRuntimeHost({ governanceProfile: "strict" })` 一次声明限流、熔断、压缩、PAE 准入与轨迹持久化。`standard` 与旧版数字逐字一致;非默认档哈希进运行指纹,跨档重放报配置漂移而非静默分叉
40
+ - **零运行时依赖** —— 纯 TypeScript strict 模式,Node.js ≥ 20 直接运行
41
+
42
+ ## 架构分层(严格单向依赖,禁止反向与循环)
43
+
44
+ ```
45
+ types(领域契约)
46
+
47
+ utils(版本解析 / ID 生成)
48
+
49
+ core(领域异常)
50
+
51
+ channel(能力通道层) pact(插件规约层)
52
+ safeguard(安全防护层) trace(轨迹溯源层)
53
+
54
+ sandbox(智能体沙箱层)
55
+
56
+ runtime_host(顶层宿主入口)
57
+ ```
58
+
59
+ 📐 详细架构图与设计决策说明:[docs/architecture.md](./docs/architecture.md) · [architecture.svg](./docs/architecture.svg)
60
+
61
+ 📜 架构宪章(三条公理 · 四档治理 · 内核准入清单):[docs/VISION.md](./docs/VISION.md)
62
+ 🗓 研发计划(三个发布波次:开源发布 → 网关确定性边界 → 生态接入):[docs/DEV_PLAN.md](./docs/DEV_PLAN.md)
63
+ 🛠 升级方案与阻断项解决:[docs/UPGRADE_PLAN.md](./docs/UPGRADE_PLAN.md)
64
+ 📈 产品规划:[docs/PRODUCT_PLAN.md](./docs/PRODUCT_PLAN.md)
65
+
66
+ ## 插件适配引擎 PAE(W15)
67
+
68
+ 外来运行时——今天为进程内 JS,后续 MCP / OpenAPI / Cordis——通过**插件适配引擎(PAE)**接入内核。适配层刻意保持极薄:适配器只持有对外来运行时的连接,并将其工具翻译为内核能力契约。两条规则让内核其余部分对"外来"毫不知情:
69
+
70
+ 1. **适配器从不直连内核**:注册后的适配器整体发布为单一能力通道(`ChannelKind.PAE_TOOL`),因此每次外来调用都走 `capabilityInvoke → ChannelHub → 注册表 → 适配器`,并落入 `RecordJournal`。外来工具与原生通道接受**同一套**四重治理校验——能力门禁、预算、跳闸保护、重放。
71
+ 2. **适配器不引入非确定性**:随机与时钟经 `PaeInvokeCtx`(`rng` / `clock`)注入;处理器若伸手去用 `Math.random` / `Date.now` 会破坏重放并被拒绝。
72
+
73
+ 能力协商是显式的,而非静默:适配器若无法无损映射某外来工具,必须借 `fidelity`(`full | reduced | lossy`)声明,调用方亦可要求最低保真度;低于 `full` 必须附带 `fidelityNote`。注册表还会把整个适配面的 `configHash` 写进运行指纹,使工具集的变更被报告为配置漂移,而非 digest 不匹配。
74
+
75
+ ```ts
76
+ import { OrbitRuntimeHost, JsPaeAdapter, ChannelKind } from "orbit-agent-runtime";
77
+
78
+ const host = new OrbitRuntimeHost();
79
+ await host.bootHost();
80
+
81
+ // 把一组进程内 JS 工具适配为内核能力通道。
82
+ const adapter = new JsPaeAdapter({
83
+ adapterId: "echo-tools",
84
+ sourceEdition: "1.0.0",
85
+ tools: [{
86
+ name: "echo",
87
+ capability: "channel:write",
88
+ handler: async (args) => ({ echoed: (args[0] as { text: string }).text })
89
+ }]
90
+ });
91
+ host.registerPaeToolAdapter(adapter); // 外来工具面 → 派生 Pact,受门禁 + 记录
92
+
93
+ // 在智能体脚本内,外来工具就是一次普通通道调用:
94
+ // const out = await ctx.call(ChannelKind.PAE_TOOL, "echo", [{ text: "hi" }]);
95
+ ```
96
+
97
+ ## 日志持久化(W27)
98
+
99
+ 日志此前只存在于内存,进程重启即擦除审计轨迹与已录制运行。现在两类日志各挂一份崩溃安全
100
+ 的预写日志——**按路径选择性开启**,不传路径则与此前的纯内存行为逐字节一致。
101
+
102
+ ```ts
103
+ const host = new OrbitRuntimeHost({
104
+ traceJournalPath: ".orbit/trace.wal.jsonl", // 审计 / 行为日志
105
+ recordJournalPath: ".orbit/record.wal.jsonl", // 录制窗口
106
+ auditRetention: 10_000 // 只保留最新 N 条
107
+ });
108
+
109
+ await host.bootHost(); // 先恢复(含自愈),再装配通道
110
+ // ... 运行智能体;上次的窗口被续开,orderIndex 顺延
111
+ await host.shutdownHost(); // 排空在途写入,再应用留存
112
+ ```
113
+
114
+ 几处值得知道的设计点:
115
+
116
+ - **崩溃模型决定了格式。** 一次写入只追加一整行,因此崩溃唯一能留下的是**被截断的末行**。
117
+ 恢复由此严格二分:丢弃那一行;而任何解析失败或结构非法的**内部行**按 `WalFileInvalidError`
118
+ 拒绝并带上行号——内部行不可能被崩溃截断,静默跳过等于隐藏真实损坏。
119
+ - **内存日志是唯一真源。** WAL 是 fire-and-forget 镜像,经写入链串行化以保证行不交错;
120
+ `shutdownHost` 会 await 该链,正常关闭不丢条目。
121
+ - **恢复逐字节一致。** `entryUid`、`occurredAt`、`orderIndex` 均被保留,故续开的录制窗口
122
+ 索引顺延而非归零——被进程边界切开的运行仍重放为一条序列。
123
+ - **截断的末行在首次追加前被治愈。** 恢复虽容忍它,但那行仍在磁盘上;一旦本次运行追加新行,
124
+ 它就变成**内部**非法行,而那是硬故障。不处理的话,一次崩溃会让此后每次启动都失败。
125
+ `healIfNeeded()` 从存活前缀原子重写文件,健康日志则零重写。
126
+ - **留存是显式的。** append-only 日志无上限增长终会打满磁盘,而磁盘满是一次宕机,因此
127
+ `auditRetention` 由运维选择而非隐式默认;`pruneAuditLog()` 供长运行实例按需裁剪。
128
+
129
+ ## Web 控制台
130
+
131
+ [Orbit Console](./web/README.md) — 零依赖的 Web 管理控制台,通过 HTTP 驱动真实内核实例:生命周期、通道、插件、沙箱、轨迹、回放台、影响域图与成本路由。
132
+
133
+ ```bash
134
+ npm run build
135
+ node web/bridge-server.mjs # http://127.0.0.1:8899
136
+ ```
137
+
138
+ ## 示例与基准(M5/M6)
139
+
140
+ **示例**(`./examples`)是可运行、带断言的走查——任一步失败即非零退出,可兼作 CI 冒烟:
141
+
142
+ ```bash
143
+ node examples/custom-channel.mjs # 实现通道 → record → replay 逐字节一致
144
+ node examples/js-pae-plugin.mjs # 外来 JS 工具经网关治理(PAE L0)
145
+ node examples/mcp-adapter.mjs # 真实 MCP 子进程,对端死后仍可逐字节重放(L2)
146
+ node examples/cli-record-replay.mjs # orbit CLI record → replay → diff 闭环
147
+ ```
148
+
149
+ **基准**(`./benchmarks`,`npm run benchmark`)对照 VISION 性能预算观测热路径:
150
+
151
+ | 套件 | 测量内容 | 样例(Node 22) |
152
+ |---|---|---|
153
+ | `gateway` | 治理化 `capabilityInvoke` 全链路(record 模式) | ~82k calls/s(~12 µs) |
154
+ | `replay` | journal 快路径注入 | ~261k calls/s(~3.8 µs) |
155
+ | `wal` | 持久化 append + flush(WAL 镜像) | ~1.5k appends/s |
156
+ | `pae` | L0 进程内 vs L2 stdio 子进程适配器延迟 | ~38 µs vs ~176 µs(4.6×) |
157
+
158
+ ## 快速开始 · 确定性重放闭环
159
+
160
+ 产品的核心卖点是**可复现**:记录一次真实运行,用**零**模型调用重放它,并证明两条调用链逐字节一致。驱动它的就是 `orbit` CLI(零额外依赖,随包发布于 `bin/`,Node ≥ 20 即可运行)。
161
+
162
+ ```bash
163
+ npm install # 仅开发依赖(typescript + @types/node)
164
+ npm run build # strict 编译 → dist/(同时产出 CLI)
165
+
166
+ # 1) 写一个脚本 —— 它拿到一个带通道访问能力的 ctx
167
+ cat > agent.mjs <<'EOF'
168
+ export default async function (ctx) {
169
+ const reply = await ctx.llm.chat("summarize: the sky is blue");
170
+ const seen = await ctx.call(ctx.ChannelKind.MEM_KV_STORE, "readEntry", "last");
171
+ return { reply, seen };
172
+ }
173
+ EOF
174
+
175
+ # 2) 记录真实运行 → 把每次通道调用捕获成轨迹
176
+ node bin/orbit.mjs record agent.mjs --out trace.jsonl
177
+ # ✓ recorded 2 channel calls from agent.mjs
178
+ # trace : trace.jsonl (JSONL,原子写)
179
+ # meta : trace.jsonl.meta.json (驱动脚本 + 脱敏后的配置)
180
+
181
+ # 3) 重放(零真实调用)→ digest 链对账
182
+ node bin/orbit.mjs replay trace.jsonl
183
+ # original calls : 2 replayed calls : 2
184
+ # result : ✓ VERIFIED — digest chain consistent
185
+
186
+ # 4) 对比两条轨迹 —— 定位首个分歧点
187
+ node bin/orbit.mjs diff trace.jsonl trace.jsonl
188
+ # result: ✓ identical call chains
189
+ ```
190
+
191
+ 四条命令就是完整产品:一个陌生人十分钟内即可复现。每条命令都支持 `--json` 机器可读输出。
192
+
193
+ ### 底层 API 与演示
194
+
195
+ ```bash
196
+ npm test # 构建 + 运行内核单元测试(node:test)—— 348 用例
197
+ npm run test:console # 运行控制台前端单测(node:test)—— 89 用例
198
+ npm run demo # 构建 + 运行演示入口(覆盖全部核心机制)
199
+ npm run demo:replay # 确定性重放:约 1s 真实运行 → 约 2ms 重放
200
+ ```
201
+
202
+ 演示输出要点:
203
+
204
+ ```
205
+ [cap] plugin -> LLM channel (channel:read): allowed
206
+ [cap] plugin -> KV write (undeclared channel:write): rejected
207
+ [sandbox] round 3 rejected (budget spent): agent sandbox box.demo-1 reached cycle limit 2
208
+ [guard] plugin crash isolated and journaled (host keeps running)
209
+ [trace] 5 entries: AGENT_SINGLE_CYCLE_EXEC / AGENT_CYCLE_LIMIT_HIT / PLUGIN_UNIT_EXCEPTION ...
210
+ ```
211
+
212
+ ## orbit CLI
213
+
214
+ 三条命令构成确定性重放闭环。CLI 通过 `createRequire` 加载已编译内核,仅用 Node 内置模块。
215
+
216
+ ```bash
217
+ orbit record <script.js> [--out trace.jsonl] [--config orbit.config.json]
218
+ orbit replay <trace.jsonl> [--via script.js] [--config orbit.config.json]
219
+ orbit diff <a.jsonl> <b.jsonl>
220
+ ```
221
+
222
+ | 命令 | 作用 | 退出码 |
223
+ |---|---|---|
224
+ | `record` | 在真实内核上运行 `<script>`,把每次通道调用捕获为 JSONL 轨迹 + `.meta.json`(驱动脚本、脱敏配置、orbit/node 版本) | 成功 0 |
225
+ | `replay` | 用**零**真实通道调用重跑脚本,并把重放链与原始链对账(银行式 digest 校验) | 一致 0 · 漂移 1 |
226
+ | `diff` | 逐记录对比两条轨迹,报告首个断点(`channelKind` / `funcName` / `inputDigest` / `outputSnapshot`) | 一致 0 · 分歧 1 |
227
+
228
+ **脚本契约** —— 默认导出一个接收 `ctx` 的异步函数:
229
+
230
+ ```js
231
+ export default async function (ctx) {
232
+ const reply = await ctx.llm.chat("hello"); // LLM_ACCESS.chatRound 的语法糖
233
+ const prev = await ctx.call(ctx.ChannelKind.MEM_KV_STORE, "readEntry", "k");
234
+ return { reply, prev };
235
+ }
236
+ ```
237
+
238
+ **配置** —— `orbit.config.json`(全部可选)选择真实能力;环境变量可覆盖以便快速试验:
239
+
240
+ ```json
241
+ {
242
+ "llm": { "kind": "mock" | "openai-compat", "baseUrl": "…", "model": "…" },
243
+ "file": { "enabled": true, "rootDir": "./agent-workspace" },
244
+ "shell": { "enabled": true, "allowedCommands": ["git","node"], "envAllowlist": ["PATH"] }
245
+ }
246
+ ```
247
+
248
+ 环境变量覆盖:`ORBIT_LLM_BASE_URL` / `ORBIT_LLM_API_KEY` / `ORBIT_LLM_MODEL`、
249
+ `ORBIT_FILE_ROOT`、`ORBIT_SHELL_ALLOW`(逗号列表)/ `ORBIT_SHELL_ENV`(逗号列表)。
250
+
251
+ > 一条录制好的轨迹,可以在**完全没有安装真实通道**的机器上重放——重放快速路径只从轨迹取数据,永远不需要 provider、凭据或工具。参见 [docs/guide.md](./docs/guide.md) 学习如何编写自己的可重放通道。
252
+
253
+ 演示输出要点:
254
+
255
+ ```
256
+ [cap] plugin -> LLM channel (channel:read): allowed
257
+ [cap] plugin -> KV write (undeclared channel:write): rejected
258
+ [sandbox] round 3 rejected (budget spent): agent sandbox box.demo-1 reached cycle limit 2
259
+ [guard] plugin crash isolated and journaled (host keeps running)
260
+ [trace] 5 entries: AGENT_SINGLE_CYCLE_EXEC / AGENT_CYCLE_LIMIT_HIT / PLUGIN_UNIT_EXCEPTION ...
261
+ ```
262
+
263
+ ## 目录结构(Monorepo)
264
+
265
+ 内核以 npm workspaces + TypeScript Project References 组织为多包仓库,各层可独立
266
+ 构建、版本与测试,而 `src/index.ts` 的公共 API 保持不变。
267
+
268
+ ```
269
+ orbit-agent-runtime/
270
+ ├── packages/
271
+ │ ├── infra-common/ # @orbit/infra-common —— 领域契约/纯工具/异常(types·utils·core)
272
+ │ ├── core-hub/ # @orbit/core-hub —— 通道/网关/replay/trace/pact/safeguard/routing
273
+ │ ├── sandbox-runtime/ # @orbit/sandbox-runtime —— 沙箱/影响域图/隔离域
274
+ │ └── pae-engine/ # @orbit/pae-engine —— 插件适配引擎(JS/MCP/OpenAPI/Cordis)
275
+ ├── src/ # 根宿主(core/orbitRuntimeHost.ts + index.ts 门面,公共 API 不变)
276
+ ├── test/ # 内核单元测试(node:test)
277
+ ├── demo-host.ts # 启动演示入口
278
+ └── web/ # 零依赖管理控制台(bridge + SPA)
279
+ ```
280
+
281
+ 依赖分层无环:`infra-common → core-hub → {sandbox-runtime, pae-engine} → host`。
282
+
283
+ ## 核心概念
284
+
285
+ | 概念 | 职责 |
286
+ |---|---|
287
+ | Channel | 外部能力的统一抽象;插件可覆盖内置通道(插件优先、内置兜底) |
288
+ | Pact | 插件清单:id、版本、宿主最低版本、能力声明 |
289
+ | TripProtector | 插件级故障状态机:连续失败跳闸 → 冷却 → 探测,单次成功即恢复 |
290
+ | TraceJournal | 追加式行为日志,快照保存/恢复,按链路/沙箱筛选 |
291
+ | AgentSandbox | 每智能体执行沙箱:循环计数、每轮独立追踪 ID、通道化模型调用 |
292
+ | SandboxPool | 沙箱生命周期管理:创建、查询、移除、释放 |
293
+
294
+ ## 设计路线图
295
+
296
+ | 里程碑 | 内容 | 状态 |
297
+ |---|---|---|
298
+ | M1 | 工程骨架:tsconfig/package/测试/演示入口/能力裁决闭环 | ✅ 已完成 |
299
+ | M2 | **确定性重放** —— 记录非确定源,重放零调用精确复现,digest 链对账 | ✅ 已完成 |
300
+ | M3 | **影响域图论内核** —— 故障隔离建模为反向可达闭包,附隔离定理,注册期能力闭包静态验证 | ✅ 已完成 |
301
+ | M4 | **成本感知路由** —— 通道成本/延迟/质量档案,沙箱按轮预算调度 | ✅ 已完成 |
302
+ | M5 | 产品化攻坚:基准测试、插件示例、CI、npm 发布 | ✅ 已完成 |
303
+ | M6 | **开源发布** —— `orbit` CLI(`record`/`replay`/`diff`) | ✅ 已完成(CLI + audit 命令已交付) |
304
+ | M6b | **开源发布** —— 文档站 / 落地页、首个公开 npm 版本 | 待办(npm publish 一步之遥;`prepublishOnly` 已配好门禁) |
305
+
306
+ > M5/M6 对应 [docs/PRODUCT_PLAN.md](./docs/PRODUCT_PLAN.md) 的 P0 里程碑
307
+ > (P0.1 真能力落地 → P0.2 CLI 发布 → P0.3 开源发布)。
308
+
309
+ ## 与主流方案的设计取舍
310
+
311
+ - **DeepSeek Harness** 走"一切皆插件"的全插件化路线(50+ 包 monorepo)。Orbit 保持**固定内核 + 通道级插件**:自由度更低,但六大机制各自独立、可完整讲解与验证
312
+ - **熔断器库**(opossum/cockatiel)在单个调用点做统计保护;Orbit 在**插件维度**隔离,且每次状态迁移绑定轨迹日志
313
+ - **MCP** 标准化工具发现;Orbit 的通道集线器是更轻的进程内等价物,自带超时、降级与能力裁决
314
+
315
+ ## 许可协议
316
+
317
+ [Apache License 2.0](./LICENSE)
318
+
319
+ ## 接入真实模型(DeepSeek)
320
+
321
+ 内置 LLM 通道为测试用模拟实现;运行时将真实 DeepSeek provider 注册为插件扩展通道即可覆盖(插件通道优先于内置):
322
+
323
+ ```ts
324
+ import { OrbitRuntimeHost, DeepSeekChannel, ChannelKind } from "orbit-agent-runtime";
325
+
326
+ const host = new OrbitRuntimeHost();
327
+ await host.bootHost();
328
+ host.channelHub.registerPluginExtChannel(
329
+ ChannelKind.LLM_ACCESS,
330
+ new DeepSeekChannel({ apiKey: process.env.DEEPSEEK_API_KEY, model: "deepseek-chat" })
331
+ );
332
+ ```
333
+
334
+ 确定性重放无需改动:先记录真实运行,再零 API 调用精确重放、输出逐字节一致。
335
+
336
+ ```bash
337
+ DEEPSEEK_API_KEY=sk-xxx npm run demo:deepseek
338
+ ```
339
+
340
+ ### 任意 OpenAI 兼容模型
341
+
342
+ `OpenAICompatChannel` 支持**任意** OpenAI 兼容端点——把 `baseUrl` 指过去即可:
343
+
344
+ ```ts
345
+ import { OpenAICompatChannel } from "orbit-agent-runtime";
346
+
347
+ // DeepSeek / OpenAI / OpenAI / 通义 / Kimi / Ollama / vLLM ...
348
+ host.channelHub.registerPluginExtChannel(ChannelKind.LLM_ACCESS, new OpenAICompatChannel({
349
+ apiKey: process.env.LLM_API_KEY,
350
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode", // 示例:通义千问
351
+ model: "qwen-plus"
352
+ }));
353
+ ```
354
+
355
+ 非 OpenAI 协议(Anthropic Claude、Google Gemini)各需一个约 20 行的 `IChannelProvider` 适配器;重放/隔离/路由机制与协议无关。
356
+
357
+ 生产化能力已内置:故障分类(`LlmChannelFaultError`——超时/网络/限流/服务端错误/鉴权/参数错误/未找到/空响应/无效响应),可重试故障用**确定性**指数退避重试(无 `Math.random`、尊重 `Retry-After`),内部重试不会泄漏进录制日志。
358
+
359
+ ### 真实工具通道(文件 / Shell)
360
+
361
+ ```ts
362
+ import { FileChannel, ShellChannel } from "orbit-agent-runtime";
363
+
364
+ // 文件访问,监禁在根目录内(路径越界即拒绝)。
365
+ host.channelHub.registerPluginExtChannel(ChannelKind.FILE_SYSTEM, new FileChannel({
366
+ rootDir: "./agent-workspace"
367
+ }));
368
+
369
+ // 命令执行,精确匹配白名单;仅接受 argv 数组(无 shell 字符串 → 无注入面),
370
+ // 子进程默认空环境、硬超时与输出上限。
371
+ host.channelHub.registerPluginExtChannel(ChannelKind.SHELL_EXEC, new ShellChannel({
372
+ allowedCommands: ["git", "node", process.execPath],
373
+ workDir: "./agent-workspace",
374
+ envAllowlist: ["PATH"]
375
+ }));
376
+ ```
377
+
378
+ 两者均为 `IO_BOUND` 通道:录制一次运行后重放,零磁盘访问、零进程启动。能力门禁映射:read/list/stat → `channel:read`,write/append/remove/mkdir/exec → `channel:write`。
379
+
380
+ ### 轨迹持久化(JSONL)
381
+
382
+ ```ts
383
+ import { saveRecordJournal, loadRecordJournal } from "orbit-agent-runtime";
384
+
385
+ await saveRecordJournal(journal, "trace.jsonl"); // 原子写(tmp + rename)
386
+ const restored = await loadRecordJournal("trace.jsonl"); // 校验加载
387
+ ```
388
+
389
+ 在**未安装真实通道**的新宿主上也能重放——重放快速路径直接从日志供给,不要求重放机器上存在通道实现、凭据或工具。
@@ -0,0 +1,24 @@
1
+ # Benchmarks
2
+
3
+ Performance observability for the kernel's hot paths, against the budgets in
4
+ [docs/VISION.md](../docs/VISION.md) (gateway overhead ≤5% of a channel call,
5
+ PAE L2 ≤15% overhead over native).
6
+
7
+ ```bash
8
+ npm run benchmark # every suite, one table
9
+ node benchmarks/gateway.mjs # one suite at a time
10
+ ```
11
+
12
+ | Suite | What it measures | Sample numbers (2026-09, Windows, Node 22) |
13
+ |---|---|---|
14
+ | `gateway.mjs` | One governed `capabilityInvoke` end to end (pact / trip / rate-limit / budget / decision / dispatch / journal), record mode | ~82k calls/s, ~12 µs/call |
15
+ | `replay.mjs` | Replay fast path — frozen outputs served straight from the journal | ~261k calls/s, ~3.8 µs/call |
16
+ | `wal.mjs` | Durable journal append → flush (in-memory append + serialised WAL mirror) | ~1.5k appends/s, ~685 µs each |
17
+ | `pae.mjs` | Adapter latency, L0 (in-process JS) vs L2 (stdio child process) | L0 ~38 µs, L2 ~176 µs, 4.6× cross-process factor |
18
+
19
+ Set `N` to scale a run: `N=100000 node benchmarks/gateway.mjs`.
20
+
21
+ Numbers are indicative, not a contract: they depend on the machine, the disk
22
+ and the channel workload. The suites are smoke-gated in CI (they must run to
23
+ completion); the budget assertions are deliberately loose so a slow shared
24
+ runner does not fail the build.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Benchmark — gateway call latency & throughput.
3
+ *
4
+ * Measures the cost of one governed `capabilityInvoke` end to end (pact check,
5
+ * trip check, rate limiter, token budget, routing decision, channel dispatch,
6
+ * journal append). The reference for VISION's performance budget is a gateway
7
+ * overhead of ≤5% of the channel call itself; the mock channel is
8
+ * deliberately trivial, so what this measures is close to the raw boundary
9
+ * cost.
10
+ *
11
+ * Run: node benchmarks/gateway.mjs
12
+ */
13
+ import { OrbitRuntimeHost, ChannelKind } from "../dist/src/index.js";
14
+ import { makeUniqueMark } from "../dist/src/index.js";
15
+
16
+ const N = Number(process.env.N || 20_000);
17
+ const warmup = Math.floor(N / 10);
18
+
19
+ async function main() {
20
+ const host = new OrbitRuntimeHost();
21
+ await host.bootHost();
22
+ host.registerPlugin({
23
+ id: "bench.gateway",
24
+ displayName: "Gateway Benchmark",
25
+ edition: "1.0.0",
26
+ requireHostMinEdition: "1.0.0",
27
+ allowCapabilities: ["channel:read"]
28
+ });
29
+ host.beginRecording(); // journal append included in every measured call
30
+
31
+ const ctx = { traceMarkId: makeUniqueMark(), pluginUnitId: "bench.gateway", maxWaitMs: 5000 };
32
+ const invoke = () =>
33
+ host.capabilityInvoke({
34
+ kind: ChannelKind.MEM_KV_STORE,
35
+ pluginId: "bench.gateway",
36
+ funcName: "readEntry",
37
+ args: ["bench-key"],
38
+ mode: "record",
39
+ ctx
40
+ });
41
+
42
+ for (let i = 0; i < warmup; i += 1) await invoke();
43
+
44
+ const t0 = process.hrtime.bigint();
45
+ for (let i = 0; i < N; i += 1) await invoke();
46
+ const t1 = process.hrtime.bigint();
47
+ const ns = Number(t1 - t0);
48
+ const perCallUs = ns / N / 1000;
49
+
50
+ console.log("=== gateway capabilityInvoke (record mode) ===");
51
+ console.log(`calls : ${N}`);
52
+ console.log(`total : ${(ns / 1e6).toFixed(1)} ms`);
53
+ console.log(`throughput : ${Math.round((N / ns) * 1e9)} calls/s`);
54
+ console.log(`p50-ish : ${perCallUs.toFixed(1)} µs/call (mean)`);
55
+
56
+ await host.shutdownHost();
57
+ // A threshold keeps the budget honest on this machine; loosen it for CI-only
58
+ // boxes, tighten it on a clean runner.
59
+ if (perCallUs > 2000) {
60
+ console.error(`WARN: gateway call above 2000µs (${perCallUs.toFixed(0)}µs) — investigate`);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ main().catch((err) => {
66
+ console.error(err);
67
+ process.exit(1);
68
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Benchmark — PAE adapter latency: L0 (in-process JS) vs L2 (stdio child).
3
+ *
4
+ * The same tool shape served two ways: in-process (L0, JsPaeAdapter) and as a
5
+ * real child process (L2, MCP over stdio). The delta is the cross-process
6
+ * round trip. VISION's budget is ≤15% overhead for an L2 adapter call against
7
+ * the native channel path; the mock channel is trivial, so the absolute
8
+ * numbers are the interesting part, not the ratio to native.
9
+ *
10
+ * Run: node benchmarks/pae.mjs
11
+ */
12
+ import { fileURLToPath } from "node:url";
13
+ import { OrbitRuntimeHost, ChannelKind } from "../dist/src/index.js";
14
+ import { JsPaeAdapter, McpPaeAdapter, StdioMcpTransport } from "../dist/src/index.js";
15
+ import { makeUniqueMark } from "../dist/src/index.js";
16
+
17
+ const PEER = fileURLToPath(new URL("../web/test/fixtures/mcp-stdio-server.mjs", import.meta.url));
18
+ const N = Number(process.env.N || 2_000);
19
+
20
+ async function time(label, fn) {
21
+ const t0 = process.hrtime.bigint();
22
+ for (let i = 0; i < N; i += 1) await fn(i);
23
+ const t1 = process.hrtime.bigint();
24
+ const us = Number(t1 - t0) / N / 1000;
25
+ console.log(`${label.padEnd(28)} ${us.toFixed(1).padStart(9)} µs/call ${Math.round((N / Number(t1 - t0)) * 1e9)} calls/s`);
26
+ return us;
27
+ }
28
+
29
+ async function main() {
30
+ const host = new OrbitRuntimeHost();
31
+ await host.bootHost();
32
+
33
+ // L0 — in-process JS adapter.
34
+ const jsAdapter = new JsPaeAdapter({
35
+ adapterId: "bench.js",
36
+ sourceEdition: "1.0.0",
37
+ isolation: "L0",
38
+ tools: [
39
+ {
40
+ name: "echo",
41
+ capability: "channel:read",
42
+ determinism: "deterministic",
43
+ fidelity: "full",
44
+ description: "echo the input",
45
+ handler: (_ctx, x) => x
46
+ }
47
+ ]
48
+ });
49
+ const jsPact = host.registerPaeToolAdapter(jsAdapter);
50
+ const jsCtx = { traceMarkId: makeUniqueMark(), pluginUnitId: jsPact.id, maxWaitMs: 5000 };
51
+ const jsInvoke = (i) =>
52
+ host.capabilityInvoke({
53
+ kind: ChannelKind.PAE_TOOL,
54
+ pluginId: jsPact.id,
55
+ funcName: "echo",
56
+ args: [`v${i}`],
57
+ mode: "live",
58
+ ctx: jsCtx
59
+ });
60
+
61
+ // L2 — real MCP child process.
62
+ const mcpAdapter = new McpPaeAdapter({
63
+ adapterId: "bench.mcp",
64
+ sourceEdition: "0.1.0",
65
+ transport: new StdioMcpTransport({ command: process.execPath, args: [PEER] })
66
+ });
67
+ const mcpPact = await host.connectPaeToolAdapter(mcpAdapter, { maxWaitMs: 10_000 });
68
+ const mcpCtx = { traceMarkId: makeUniqueMark(), pluginUnitId: mcpPact.id, maxWaitMs: 10_000 };
69
+ const mcpInvoke = (i) =>
70
+ host.capabilityInvoke({
71
+ kind: ChannelKind.PAE_TOOL,
72
+ pluginId: mcpPact.id,
73
+ funcName: "greet",
74
+ args: [{ name: `v${i}` }],
75
+ mode: "live",
76
+ ctx: mcpCtx
77
+ });
78
+
79
+ console.log("=== PAE adapter latency (per governed call) ===");
80
+ const l0 = await time("L0 JS (in-process)", jsInvoke);
81
+ const l2 = await time("L2 MCP (stdio child)", mcpInvoke);
82
+ console.log(`\nL2 / L0 cross-process factor: ${(l2 / l0).toFixed(1)}×`);
83
+
84
+ await host.shutdownHost();
85
+ }
86
+
87
+ main().catch((err) => {
88
+ console.error(err);
89
+ process.exit(1);
90
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Benchmark — replay throughput.
3
+ *
4
+ * The replay fast path serves frozen outputs straight from the journal: no
5
+ * providers, no credentials, no tools. Measures how many replay calls a single
6
+ * process can serve per second, which is the realistic ceiling for
7
+ * reconciliation-driven verification pipelines.
8
+ *
9
+ * Run: node benchmarks/replay.mjs
10
+ */
11
+ import { OrbitRuntimeHost, ChannelKind } from "../dist/src/index.js";
12
+ import { ReplayEngine, RecordJournal, digestInputs } from "../dist/src/index.js";
13
+ import { makeUniqueMark } from "../dist/src/index.js";
14
+
15
+ const N = Number(process.env.N || 100_000);
16
+
17
+ function seedJournal(count) {
18
+ const journal = new RecordJournal();
19
+ for (let i = 0; i < count; i += 1) {
20
+ journal.append({
21
+ channelKind: ChannelKind.MEM_KV_STORE,
22
+ funcName: "readEntry",
23
+ inputDigest: digestInputs(`k${i}`),
24
+ outputSnapshot: `v${i}`,
25
+ durationMs: 0
26
+ });
27
+ }
28
+ return journal;
29
+ }
30
+
31
+ async function main() {
32
+ const journal = seedJournal(N);
33
+ const engine = new ReplayEngine(journal);
34
+
35
+ const t0 = process.hrtime.bigint();
36
+ for (let i = 0; i < N; i += 1) {
37
+ engine.replayCall(ChannelKind.MEM_KV_STORE, "readEntry", digestInputs(`k${i}`), i);
38
+ }
39
+ const t1 = process.hrtime.bigint();
40
+ const ns = Number(t1 - t0);
41
+
42
+ console.log("=== replay fast path ===");
43
+ console.log(`calls : ${N}`);
44
+ console.log(`throughput : ${Math.round((N / ns) * 1e9)} calls/s`);
45
+ console.log(`per call : ${(ns / N / 1000).toFixed(2)} µs`);
46
+ }
47
+
48
+ main().catch((err) => {
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Benchmark runner — every suite, one summary table.
3
+ *
4
+ * Run: node benchmarks/run-all.mjs (or: npm run benchmark)
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import * as path from "node:path";
9
+
10
+ const REPO = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
11
+ const SUITES = ["gateway", "replay", "wal", "pae"];
12
+
13
+ console.log("Orbit Agent Runtime · benchmarks\n");
14
+ for (const suite of SUITES) {
15
+ const file = path.join(REPO, "benchmarks", `${suite}.mjs`);
16
+ const res = spawnSync(process.execPath, [file], { cwd: REPO, encoding: "utf8" });
17
+ if (res.status !== 0) {
18
+ console.error(res.stderr || res.stdout);
19
+ process.exit(res.status ?? 1);
20
+ }
21
+ console.log(res.stdout.trim());
22
+ console.log("");
23
+ }
24
+ console.log("done");