@onlyne/lib 0.0.0-stage → 3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dbydd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # @onlyne/lib
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Onlyne v3 library: threads, durable message admission, attention obligations, reply correlation, resources with explicit handover, and a Fetch-compatible HTTP binding.
4
+
5
+ The protocol is specified by the draft RFCs in [`docs/rfcs`](../../docs/rfcs). The RFC-level formalization lives in [`proof/proof_rfc`](../../proof/proof_rfc); the implementation-level one in `proof/proof_lib`.
6
+
7
+ ## Run
8
+
9
+ The package exports TypeScript source for a TypeScript-capable host such as Bun.
10
+
11
+ ```sh
12
+ bun install
13
+ bun run check
14
+ bun run test
15
+ ```
16
+
17
+ `bun run test` runs the public API verification script, including its loopback HTTP nodes. From the repository root, `bun run check` and `bun run test` delegate to this package.
18
+
19
+ ## Library
20
+
21
+ The host supplies the node identity, service URL, and plugins. The library exposes a Fetch handler. The host owns the listener and its shutdown.
22
+
23
+ ```ts
24
+ import { OnlyneNode, type NodeOptions } from '@onlyne/lib';
25
+
26
+ export function serveOnlyne(options: NodeOptions, port: number) {
27
+ const node = new OnlyneNode(options);
28
+ const server = Bun.serve({
29
+ hostname: '127.0.0.1',
30
+ port,
31
+ fetch: node.fetch,
32
+ });
33
+ return { node, server };
34
+ }
35
+ ```
36
+
37
+ `options.base_url` includes `/onlyne/v3`. `FetchTransport` permits HTTP on loopback and requires HTTPS for remote hosts. The resolver supplies the remote identity, URL, and authentication headers. The authentication plugin maps each inbound request to its verified actor.
38
+
39
+ The model calls `node.client.msg({ delivery_kind, msg, meta })`. The `decide` plugin returns a typed send, read, thread, resource, or domain action. An unavailable decision returns `decision_unavailable`. Hosts can also call the typed `threads`, `messages`, `resources`, `domains`, and `client.send` interfaces directly.
40
+
41
+ A plan whose `response` is `await` keeps `client.send` on the reply. The outcome reports `wait` as `answered`, `timed_out`, or `cancelled`, and an answered one carries `reply` with `reply_body`. The awaited tool result and the automatic attention fragment share one consumption record per `(context, ThreadRef, seq)`. A body already confirmed in that context comes back as `reply_body: 'withheld'` with the arrival facts and the original MessageRef only; a body this result rendered closes the record, so the fragment does not present it again. `runtime.confirmToolResult` reports the tool result the host observed in that context. A record no path confirmed stays open and is replayed.
42
+
43
+ ## Domains
44
+
45
+ A domain is a named group of nodes. It has no owner, no central member table, and no implied thread or resource permission. A node belongs to one domain at a time: joining another requires leaving the current one. Threads may span domains; resource reach still goes through the resource owner or an existing ingress. Each node records only its own membership:
46
+
47
+ ```ts
48
+ const created = await node.domains.create(node.auth, mutation);
49
+ await node.domains.join(node.auth, "urn:onlyne:domain:research", mutation);
50
+ const members = await node.domains.discover(node.auth, "urn:onlyne:domain:research");
51
+ ```
52
+
53
+ `create`, `join`, and `leave` change this node's record and require its own actor. `create` or `join` for a different domain while an active membership exists fails with `conflict`. `describe` returns the local record, including a `left` tombstone. `list` returns at most one active membership. `discover` asks the host's `domainCandidates(domain_id)` for candidate node identities, queries each candidate's `/domains/{domain_id}`, and returns only the nodes that currently declare themselves active. Failed lookups land in `unavailable`; no cache or peer list is treated as the member truth.
54
+
55
+ Membership survives a node rebuild over the same storage. A repeated `join` with the current revision is idempotent. A stale `expected_revision` is refused. Retrying an earlier mutation key returns the original result without reviving a later `leave`. The HTTP routes are `POST /domains`, `GET /domains`, `GET /domains/{domain_id}`, `POST /domains/{domain_id}/join`, `POST /domains/{domain_id}/leave`, and `GET /domains/{domain_id}/members`. An authenticated service description carries the single active id in `domain`; an unauthenticated one carries neither `domain` nor `domains`.
56
+
57
+ ## Resources
58
+
59
+ A resource has one binding — the handler and the highest event it has confirmed — plus an append-only log. Events carry no recipients: an event belongs to the resource, and the binding says who is expected to consume it. `receive` appends a platform event once per `source_id`; `log` pages the log from a `since` position and `event` reads one entry. Both are authorized for the current handler and for the resource's `log_readers`, and they read storage only, so history stays available while the handler is offline. `ack` moves the handler's cursor forward and answers `{ resource_ref, cursor }`; an id at or behind the cursor changes nothing.
60
+
61
+ `client.readResources(query)` reads local resources or requests their owner's authenticated endpoint. The `ResourceReadQuery` operations are `list`, `describe`, `log`, `event`, `operations`, and `status`. The `decide` plugin can return `{ kind: 'read', scope: 'resources', query }` to use the same reader through `msg`. Reads create no operation record and do not call a resource driver.
62
+
63
+ A queued entry that projects a resource event (`meta.data.resource_ref`, authenticated by that resource's owner) is checked against the binding before it is injected. If the binding names another handler, the row is settled with `settlement.via === 'handover'`; if this node may no longer read the resource, the row is settled as `undeliverable` with `reason: 'resource-unavailable'`. A transient failure leaves the row owed and is traced as `inbound.resource-check.failed`. Settlement closes the local delivery obligation. It records why delivery ended, and it does not assert that the model read the entry, so no consumption record and no todo are written.
64
+
65
+ `list` accepts concrete `owners`, `page_size`, and `cursor`. Omitted `owners` selects the local node. The result contains `owners: [{ owner, page } | { owner, error }]`, so an unavailable owner remains visible beside successful pages. A cursor selects exactly one owner. The host chooses which owners to include. Other queries name `resource: { owner, id }`. `log` accepts an exclusive `since` event id, `page_size`, and `cursor`. `event` names `event_id`; `status` names `operation_id` and returns the existing operation record. The original actor can read that record after losing resource-read access. `operations` returns only the current actor's records.
66
+
67
+ An operation result is `committed`, `pending`, `aborted`, or `unknown`. `aborted` means the endpoint refused or locally stopped the call and confirmed that no external effect happened; a failure whose side effect cannot be ruled out is `unknown`, keeps the original error in `details`, and stays owed to `node.resources.recover()`, which asks the driver for the outcome by `operation_id`. A caller that stops waiting gets `unknown` while the external call keeps running, and a later driver outcome settles that record. A retried operation is asked for its outcome, never executed a second time.
68
+
69
+ A handover is a three-phase handshake, not an invitation. The handler posts `operation: 'handover'` with `{ target, handover_deadline, metadata? }`; the endpoint records the intent and freezes inbound delivery at the current cursor before it probes `POST /handover/probes` on the target. The target answers `accept` with a cursor or `reject` with a reason, and records that answer by `operation_id`. Acceptance switches the binding in one transaction — new handler, new revision, cursor from the probe — so the former handler's send is refused on the old revision and events written during the freeze belong to the new handler. The probe is signed by the endpoint, not by the agent that asked for the handover, so the endpoint itself needs a key exchange with the target: without one the request is never sent and the handover aborts with `data.reason: 'untrusted_target'`, its `error` naming the missing pairing and the side that must start it. A refused, unreachable, or expired probe aborts the handover and leaves the binding, revision, and cursor untouched. A probe past the caller's wait answers `unknown`; `node.resources.recover()` reads the target's recorded answer back and settles it. The switch never edits `log_readers`: history access is a separate grant.
70
+
71
+ The routes are `GET|POST /resources`, `GET /resources/{owner}/{id}`, `POST /resources/{owner}/{id}/operations`, `GET /resources/{owner}/{id}/operations[/{operation_id}]`, `POST /resources/{owner}/{id}/events`, `POST /resources/{owner}/{id}/events/{event_id}/ack`, `GET /resources/{owner}/{id}/log?since=&limit=`, `GET /resources/{owner}/{id}/log/{event_id}`, and, on the target node, `POST /handover/probes` with `GET /handover/probes/{operation_id}`. Platform SDKs, platform credentials, and artifact readers remain in resource plugins.
72
+
73
+ ## Plugin boundary
74
+
75
+ The library performs no filesystem operations. Persistence, configuration loading, credentials, attachment access, runtime integration, and platform operations belong to the host plugins. Message parts and resource data carry opaque JSON references. The library does not open the paths they contain.
76
+
77
+ | Plugin | Host responsibility |
78
+ |---|---|
79
+ | `storage` | Atomic, serializable transactions and the declared admission guarantee. A rejected transaction callback rolls back all writes. |
80
+ | `authenticate` | Verify the request credentials and return the actor plus any authorized forwarding identity. |
81
+ | `resolve` | Resolve a node identity to its service URL and request headers. |
82
+ | `decide` | Translate message intent into one executable action. |
83
+ | `runtime` | Check delivery support, inject into a host context, optionally create a runtime todo, and report an `await` tool result the host observed. |
84
+ | `resources` | Validate resource parameters, execute platform operations, and query uncertain outcomes. |
85
+ | `prepareResourceOperation` | Adopt a dynamic resource after HTTP authentication and operation parsing, before resource authorization and execution. |
86
+ | `mayAdmit`, `handoverProbe`, `mention`, `directory` | Supply admission policy, the target-side answer to a handover probe, thread aliases, and the host directory. |
87
+ | `domainCandidates` | List the node identities that may declare membership in a domain. Without it, discovery is refused rather than guessed. |
88
+ | `transport` | Optionally replace the built-in Fetch transport with another network binding. |
89
+
90
+ `MemoryStorage` is process-local and returns `binding-defined` receipts. A durable plugin returns `durable` after its transaction commit reliably stores the message, receipt, and obligations. The library does not upgrade a plugin's guarantee.
91
+
92
+ The host calls `node.client.maintain()` to retry grant notices, reconcile attention, and inject queued entries. It calls `node.client.recover()` to resume outbound requests and `node.resources.recover()` to settle resource recovery. The host chooses when these calls run and retains the storage plugin across node reconstruction. Runtime injection reports consumption before the library records an entry as injected.
93
+
94
+ The library stores no task state. Turning a message into a runtime todo is the host's: the todo snapshot and the message-reference association go into one recoverable commit, the host projection derives from that commit, and recovery replays it before creating another entry. `runtime.todo` is called once consumption is recorded; todo content and reminders stay with the host.
@@ -0,0 +1,92 @@
1
+ # @onlyne/lib
2
+
3
+ Onlyne v3 的库:thread、持久接纳的消息、关注义务、回复关联、带显式交接的资源,以及 Fetch 形式的 HTTP 绑定。
4
+
5
+ 协议草案见 [`docs/rfcs`](../../docs/rfcs)。RFC 层形式化在 [`proof/proof_rfc`](../../proof/proof_rfc),实现层形式化在 `proof/proof_lib`。
6
+
7
+ ## 运行
8
+
9
+ 包导出 TypeScript 源码,由 Bun 等支持 TypeScript 的宿主加载。
10
+
11
+ ```sh
12
+ bun install
13
+ bun run check
14
+ bun run test
15
+ ```
16
+
17
+ `bun run test` 通过公开接口运行验收脚本,并启动回环 HTTP 节点。在仓库根目录执行 `bun run check` 与 `bun run test` 会转发到本包。
18
+
19
+ ## 库入口
20
+
21
+ 宿主提供节点身份、服务地址和插件。库提供 Fetch handler,宿主负责监听端口和关闭服务。
22
+
23
+ ```ts
24
+ import { OnlyneNode, type NodeOptions } from '@onlyne/lib';
25
+
26
+ export function serveOnlyne(options: NodeOptions, port: number) {
27
+ const node = new OnlyneNode(options);
28
+ const server = Bun.serve({
29
+ hostname: '127.0.0.1',
30
+ port,
31
+ fetch: node.fetch,
32
+ });
33
+ return { node, server };
34
+ }
35
+ ```
36
+
37
+ `options.base_url` 包含 `/onlyne/v3`。`FetchTransport` 允许回环 HTTP,远端地址必须使用 HTTPS。解析插件提供远端身份、地址和认证请求头,认证插件把入站请求映射为已验证的主体。
38
+
39
+ 模型调用 `node.client.msg({ delivery_kind, msg, meta })`。`decide` 插件返回一个带类型的发送、读取、thread、资源或域动作。判定不可用时返回 `decision_unavailable`。宿主也可以直接调用 `threads`、`messages`、`resources`、`domains` 和 `client.send` 接口。
40
+
41
+ 计划的 `response` 为 `await` 时,`client.send` 会等到回复到达或等待结束。结果用 `wait` 表示 `answered`、`timed_out` 或 `cancelled`;有答复时还带 `reply` 与 `reply_body`。await 工具结果与自动关注片段共用同一条 `(context, ThreadRef, seq)` 消费记录。该上下文已确认过的正文以 `reply_body: 'withheld'` 返回,只给到达事实与原 MessageRef;本次工具结果展示过的正文会关闭该记录,自动片段不再重复展示。`runtime.confirmToolResult` 供宿主报告它已在该上下文中观察到工具结果。没有任何路径确认的记录保持未确认,会被重放。
42
+
43
+ ## 域
44
+
45
+ 域是一组节点的命名划分。它没有 owner、中心成员表,也不隐含 thread 或资源权限。一个节点同一时间只属于一个域,加入另一个域前必须先退出当前域。thread 可以跨域;资源访问仍走资源 owner 或已有的 ingress。每个节点只记录自己的成员关系:
46
+
47
+ ```ts
48
+ const created = await node.domains.create(node.auth, mutation);
49
+ await node.domains.join(node.auth, "urn:onlyne:domain:research", mutation);
50
+ const members = await node.domains.discover(node.auth, "urn:onlyne:domain:research");
51
+ ```
52
+
53
+ `create`、`join`、`leave` 只修改本节点记录,调用主体必须是本节点。已有 active 域时再创建或加入另一个域返回 `conflict`。`describe` 返回本节点当前记录,包含已退出的墓碑;`list` 至多返回一条 active 关系。`discover` 从宿主的 `domainCandidates(domain_id)` 取得候选项,逐个查询候选的 `/domains/{domain_id}`,只返回当前声明 active 的节点。查询失败的节点进入 `unavailable`,缓存或对端目录不被当作成员依据。
54
+
55
+ 使用同一存储重建节点后成员关系仍在。带当前 revision 的重复 `join` 幂等;过期 `expected_revision` 被拒。重投旧幂等键返回原结果,不会复活之后的退出。HTTP 路径为 `POST /domains`、`GET /domains`、`GET /domains/{domain_id}`、`POST /domains/{domain_id}/join`、`POST /domains/{domain_id}/leave`、`GET /domains/{domain_id}/members`。已认证的服务描述用 `domain` 返回唯一的 active 标识;未认证响应既不返回 `domain` 也不返回 `domains`。
56
+
57
+ ## 资源
58
+
59
+ 资源只有一个绑定——处理者与它已确认的最高事件——加一份只追加的日志。事件不带收件人:事件属于资源,绑定只说明该由谁消费。`receive` 按 `source_id` 去重追加平台事件;`log` 从 `since` 位置分页读日志,`event` 读单条。两者只对当前 handler 和资源的 `log_readers` 开放,且只读存储,所以 handler 离线时历史仍可读。`ack` 把 handler 的游标向前推进,返回 `{ resource_ref, cursor }`;指向游标位置或更早的 id 不改变任何状态。
60
+
61
+ `client.readResources(query)` 读取本地资源,或向资源 owner 的认证接口发起查询。`ResourceReadQuery` 支持 `list`、`describe`、`log`、`event`、`operations` 和 `status`。`decide` 插件可返回 `{ kind: 'read', scope: 'resources', query }`,让 `msg` 使用同一读取实现。查询不建立操作记录,也不调用资源驱动。
62
+
63
+ `list` 接受具体的 `owners`、`page_size` 和 `cursor`,省略 `owners` 时只查本节点。结果为 `owners: [{ owner, page } | { owner, error }]`,成功分页与不可用节点的错误分别保留。带游标的查询只允许一个 owner。宿主负责选择 owner 集合。其他查询指定 `resource: { owner, id }`。`log` 接受不包含起点的 `since` 事件编号、`page_size` 和 `cursor`;`event` 指定 `event_id`;`status` 指定 `operation_id` 并返回已有操作记录。原调用者失去资源读取权限后,仍可读取自己提交的操作记录。`operations` 只列出当前调用者的记录。
64
+
65
+ 操作结果为 `committed`、`pending`、`aborted` 或 `unknown`。`aborted` 表示 endpoint 明确拒绝或本地中止,且已确证没有产生外部效果;无法排除已产生副作用的失败记为 `unknown`,在 `details` 中保留原始错误,并由 `node.resources.recover()` 按 `operation_id` 向插件查询结局。调用方停止等待时只结束本地等待,外部调用继续运行,之后插件给出的结局可以修正该记录。重投的操作只查询结局,不会再次执行。
66
+
67
+ 交接是三段式握手,不是邀请。handler 提交 `operation: 'handover'`,数据为 `{ target, handover_deadline, metadata? }`;endpoint 先落 intent,并在当前游标处冻结入站投递,然后向目标发 `POST /handover/probes`。目标答复 `accept`(带游标)或 `reject`(带原因),并按 `operation_id` 记录答复。接受后在一个事务里切换绑定——新 handler、新 revision、探测给出的游标——因此原 handler 的发送会因旧 revision 被拒,冻结期写入的事件归新 handler。探测由 endpoint 签名,不由发起交接的 agent 签名,所以 endpoint 自己必须与目标完成密钥交换:缺少这段配对时请求根本发不出去,交接以 `data.reason: 'untrusted_target'` 中止,`error` 指明缺失的配对与该由哪一方发起。拒绝、不可达或期限到期都会中止交接,绑定、revision 与游标保持不变。探测超出调用方等待窗口时返回 `unknown`,`node.resources.recover()` 读回目标记录的答复并结案。切换不修改 `log_readers`:历史读取是另一份授权。
68
+
69
+ 路由为 `GET|POST /resources`、`GET /resources/{owner}/{id}`、`POST /resources/{owner}/{id}/operations`、`GET /resources/{owner}/{id}/operations[/{operation_id}]`、`POST /resources/{owner}/{id}/events`、`POST /resources/{owner}/{id}/events/{event_id}/ack`、`GET /resources/{owner}/{id}/log?since=&limit=`、`GET /resources/{owner}/{id}/log/{event_id}`,目标节点上另有 `POST /handover/probes` 与 `GET /handover/probes/{operation_id}`。平台 SDK、平台凭据和产物读取由资源插件负责。
70
+
71
+ ## 插件边界
72
+
73
+ 库不执行文件系统操作。持久化、配置加载、凭据、附件读取、运行时接入和平台操作由宿主插件负责。消息 parts 和资源 data 只携带 JSON 引用,库不打开引用里的路径。
74
+
75
+ | 插件 | 宿主负责的行为 |
76
+ |---|---|
77
+ | `storage` | 提供原子、可串行化的事务并声明接纳保证;事务回调失败时回滚全部写入。 |
78
+ | `authenticate` | 验证请求凭据,返回主体及明确授权的转发身份。 |
79
+ | `resolve` | 把节点身份解析为服务地址和认证请求头。 |
80
+ | `decide` | 把消息意图解释为一个可执行动作。 |
81
+ | `runtime` | 检查投递能力、注入宿主上下文、按需建立运行时待办,并报告宿主已观察到的 await 工具结果。 |
82
+ | `resources` | 校验资源参数、执行平台操作、查询未知结果。 |
83
+ | `prepareResourceOperation` | HTTP 认证并解析操作后,按需创建动态资源;资源授权与执行随后由库处理。 |
84
+ | `mayAdmit`、`handoverProbe`、`mention`、`directory` | 提供接纳策略、目标侧对交接探测的答复、thread 别名和宿主目录。 |
85
+ | `domainCandidates` | 提供可能声明某个域的节点身份。缺少它时拒绝发现,不猜成员。 |
86
+ | `transport` | 按需用其他网络绑定替换内置 Fetch transport。 |
87
+
88
+ `MemoryStorage` 只保存在当前进程中,回执保证为 `binding-defined`。持久化插件在事务可靠提交消息、回执和义务后返回 `durable`。库按插件声明的保证生成回执。
89
+
90
+ 宿主调用 `node.client.maintain()` 重投授权通知、对账关注义务并注入队列中的条目。宿主调用 `node.client.recover()` 恢复出站请求,调用 `node.resources.recover()` 推进资源恢复。调用时机由宿主决定,重建节点时继续使用原存储插件。运行时报告消息已被消费后,库才记录注入完成。
91
+
92
+ 库不保存任何任务状态。把一条消息变成运行时待办是宿主的事:待办快照与消息引用的关联写入同一个可恢复提交,宿主投影由该提交派生,恢复时先重放该提交再决定是否新建条目。消费记录完成后才调用 `runtime.todo`,待办内容和提醒由宿主负责。
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@onlyne/lib",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "3.1.0",
4
+ "description": "Decentralized agent messaging: threads, durable admission, attention, reply correlation, resources with handover, and an HTTP binding",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/dbydd/onlyne.git",
12
+ "directory": "packages/lib"
13
+ },
14
+ "homepage": "https://github.com/dbydd/onlyne#readme",
15
+ "bugs": "https://github.com/dbydd/onlyne/issues",
16
+ "keywords": ["agent", "messaging", "decentralized", "threads", "resources", "protocol"],
17
+ "publishConfig": { "access": "public" },
18
+ "engines": { "bun": ">=1.4.3" },
19
+ "exports": {
20
+ ".": "./src/index.ts",
21
+ "./memory": "./src/memory.ts"
22
+ },
23
+ "files": ["src", "README.md", "LICENSE"],
24
+ "scripts": {
25
+ "check": "tsc --noEmit",
26
+ "test": "bun tests/verify.ts && bun tests/domain.ts && bun tests/first-contact.ts"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "^1.3.0",
30
+ "typescript": "^5.9.0"
31
+ }
32
+ }