pi-onlyne 1.1.2 → 1.2.1

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.zh.md CHANGED
@@ -6,7 +6,7 @@
6
6
  `node:net` 上重写,四字节大端长度前缀加 UTF-8 JSON 的编解码是手写的,运行时零 npm 依赖。
7
7
 
8
8
  扩展在 onlyne 之外完全静默。客户端 spawn 进程时会注入 `ONLYNE_ROLE`、`ONLYNE_SESSION_ID`、
9
- `ONLYNE_TASK_ID`(`crates/onlyne-client/src/dispatch.rs`);三者缺一,就是普通 pi session,
9
+ `ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`);三者缺一,就是普通 pi session,
10
10
  插件不注册任何工具、不打开任何 socket。
11
11
 
12
12
  ```
@@ -19,10 +19,13 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
19
19
  ├─ prose ──► 注入 pi 上下文一次(custom message,不触发 turn)
20
20
  ├─ report.ready ──► 载荷等待的那道 barrier
21
21
  ◀── assign{envelope, prose, task_id, generation}
22
- ├─ 任务文本(含图片路径)──► pi user message(deliverAs:"followUp")
22
+ ├─ task text (+ image path) ──► pi user message(deliverAs:"followUp")
23
23
  ├─ assign_ack{accepted:true}
24
- ├─ report.heartbeat{running|idle} —— 每个 turn,以及任务存续期间每 10 秒
25
- ├─ report.complete{outcome, head} —— ledger 的终态事实
24
+ ├─ report.heartbeat{agent} —— 每个 turn 以及任务存续期间每 10 秒发 `running`,
25
+ │ 只有 pi 等待输入时才发 `idle`;每次心跳都重新向 pi 推导
26
+ ├─ 任务仍开着、pi 正在等待输入却没有 `onlyne_complete` ──► idle 阶梯:
27
+ │ 同一条消息再注入(最多 `idleReminders` 次),之后 `failed` 并退出
28
+ ├─ report.complete{outcome, head} —— ledger 的终态事实,也是最后一份报告
26
29
  │ └─ client 的应答就是交接点:插件据此让 pi 退出,随后 detach
27
30
  ├─ probe ──► 一条 heartbeat
28
31
  ◀── recycle ──► (未终态则先 complete)→ 停插件 → pi 退出
@@ -95,8 +98,9 @@ pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
95
98
  | --- | --- | --- |
96
99
  | `enabled` | `true` | `false` 时该工作区禁用扩展 |
97
100
  | `watch.autoStart` | `true` | `false` 时注册工具但不建连接,需 `/onlyne connect` |
101
+ | `idleReminders` | `2` | 一次空闲期内重发任务的次数上限(§4);`0` 表示第一次空闲就判失败 |
98
102
 
99
- 文件缺失即两个默认值。文件格式错误时打印一行警告,并保留默认值:一个笔误不该静默关掉一个
103
+ 文件缺失即取各自默认值。文件格式错误时打印一行警告,并保留默认值:一个笔误不该静默关掉一个
100
104
  role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为 generate 期模板提示),所以
101
105
  只有本扩展消费它;键名沿用模板里既有的形状。
102
106
 
@@ -143,41 +147,75 @@ png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成
143
147
 
144
148
  ### `onlyne_complete{outcome?, text?, force?, reason?}`
145
149
 
146
- 显式结束当前任务,`outcome` 缺省 `done`,也可 `failed`。`text` 非空时就是 ledger 的 `head`,
150
+ 显式结束当前任务,`outcome` 缺省 `done`,也可 `failed`。它是通向 `done` 的唯一路径:turn 结束时
151
+ 没有这次调用,任务会被重发提醒,之后判失败(§4)。`text` 非空时就是 ledger 的 `head`,
147
152
  原样写出:空白折叠成单行,截到 200 字符。`text` 缺失或全空白时不带摘要,completion 退回
148
153
  最后一段 assistant 文本。这一调用同时结束所在 session 的进程。client 应答完 completion
149
154
  报告(见 §4)之后,插件通过 `ctx.shutdown()` 让 pi 退出。pi 0.85.1 没有 tool-result
150
155
  `terminate` 处理。工作区带接力策略(§5)时,`force: true` 加非空 `reason` 是绕过一个仍欠着的
151
156
  接力的正规通道。
152
157
 
158
+ ### `onlyne_handoff{to, text, image?}`
159
+
160
+ 把本会话手上的任务交给家族的下一跳。插件发一个 `handoff` 帧,帧里点名本会话当前持有的任务,
161
+ 宿主据此为 `to` 铸一个该家族的子任务:子任务把本任务记为 `parent_task`,hop 加一,家族 id、
162
+ hop 预算、origin、deadline 与 labels 一并随行。工具结果给出子任务 id 与它的 hop。client 拒绝时
163
+ 以工具错误原样抛出。`image` 与 send 工具同一含义:png/jpeg/gif/webp 图片的绝对路径。家族带
164
+ hop 预算时,注入的标题行写明 hop 与预算。`onlyne_send{kind: "task"}` 是触达 role 的另一条路:
165
+ 那条 envelope 开一个新家族,hop 从 0 起。
166
+
153
167
  ## 4. outcome 判定规则
154
168
 
155
- 插件每个任务只发一次 completion,取以下三者的先到者:
169
+ `onlyne_complete` 是通向 `done` 的唯一路径。插件每个任务只发一次 completion,取以下四者的先到者:
156
170
 
157
- 1. **`onlyne_complete`** —— 模型给显式 outcome,优先级最高;同一任务的第二次 completion 被
158
- 拒(不重报)。`text` 非空时即 head,原样写出。
159
- 2. **`agent_settled`** —— pi 不会自己继续:没有待重试、待压缩或排队续跑。此时:
160
- - turn 以 provider 错误告终(`stopReason: "error"`)→ `failed`,错误信息当 head;
161
- - 其余 → `done`,最后一段 assistant 文本当 head;
162
- - 任务已投递但还没跑过任何 turn → 不发 completion。注入的消息尚未执行,这时报终态就是撒谎。
163
- 3. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
171
+ 1. **`onlyne_complete`** —— 模型给显式 outcome(缺省 `done`,也可 `failed` / `cancelled`)。同一
172
+ 任务的第二次 completion 被拒(不重报)。`text` 非空时即 head,原样写出。
173
+ 2. **turn 出错** —— turn 以 provider 错误告终(`stopReason: "error"`)。这本身就是证据,插件立即
174
+ 报 `failed`,错误信息当 head。
175
+ 3. **idle 阶梯** —— 某轮干净结束却没有 completion,pi 正在等待输入,任务还开着。插件重发任务并
176
+ 记一次;当空闲发现 `idleReminders` 给的次数已经用完,就报 `failed`(head 为 `no completion
177
+ after <n> idle reminders`),并像任何一次 completion 一样退出 session。
178
+ 4. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
164
179
  插件并退出 pi。
165
180
 
166
- `head` 恒为单行、上限 200 字符,与 client 写入 `out_head` 和回执携带的内容一致。每个任务的
167
- head 只有一个来源:显式 `onlyne_complete` 带的 `text`(有则原样采用),否则是最后一段
168
- assistant 文本。自动规则就是那条退路:它报的是自己那一轮的文字,工具调用之后再说的话,顶不掉
169
- 调用交出的内容。
181
+ 两种情况都不结算:任务已投递但还没跑过任何 turn(注入的消息尚未执行,这时报终态就是撒谎),
182
+ 以及阶梯还有余额的那次空闲。阶梯重发的是任务到手时的那条消息——同样的头部、任务文本和附件路径,
183
+ 外加一行说明上一轮没有 completion——但不重发 role prose,它已经在上下文里;图片也不重挂,
184
+ 路径以文本出现,同样的字节不会进上下文两次。同一任务收到新 envelope 时计数重来;session 自己
185
+ 跑起来的任何一轮也把计数清零(见下面的空闲判定)。
186
+
187
+ `head` 恒为单行、上限 200 字符,与 client 写入 `out_head` 和回执携带的内容一致。每个任务的 head
188
+ 只有一个来源:显式 `onlyne_complete` 带的 `text`(有则原样采用)、出错 turn 报的错误,或阶梯自己
189
+ 的那一行。最后一段 assistant 文本只是 `text` 完全缺失的 `onlyne_complete` 的退路——调用之后再说
190
+ 的话顶不掉调用交出的内容,此外没有任何东西读它。
170
191
 
171
192
  报出去的 completion 会结束所在 session 的进程。`report.complete` 以请求形式发出,client 只有
172
193
  在结算 session 行、ack 掉投递、并写好 `Completion` envelope 之后才应答,插件就在这个应答处
173
194
  让 pi 退出。socket 当时送不出去的 outcome 会被记住,并在下一次 `hello` 后补发,那次补发的
174
195
  应答就是结束进程的交接点。被宿主拒掉的 completion 不会让进程退出,任务不会因为退出而丢失。
175
196
 
176
- 最后一条上报是:在已结算的 outcome 旁边带一个 `agent: "idle"` 的观测,发在 completion 被
177
- ack 之后、进程退出之前。completion 是按 client 手里的元组结算 session 行的,而收尾那一轮
178
- 就是最后一次 heartbeat 时,这个元组读到的仍是 `running`;此后没有任何东西再观测这个进程,
179
- 所以缺了这条上报,已退出的 session 会一直说 `running`。最后一次心跳本来就是 idle 时,插件
180
- 跳过这条;已结算的观测被拒,也不拖着 completion 挣来的那次退出不走。
197
+ completion 是插件最后一份上报;已经交掉任务的 session 不再发心跳,也不再应答 `probe`。终态的
198
+ agent 维度由 client 自己写:随后到达的 `detach` 会退役这个已无任务的 session,该路径会喂入
199
+ `AgentGone` 并发布这一行(`retire_idle_locked`,
200
+ `crates/onlyne-client/src/session/dispatch/retire.rs`)。正在离开的进程不给自己声明阶段。
201
+
202
+ ### 空闲判定
203
+
204
+ 只有 session 在等待用户输入时才算空闲。其余时刻一律读作 `running`:正在跑的 turn、已排队的
205
+ steer 或 follow-up 消息、重试、压缩,以及被后台任务扩展移出 agent 循环的工作。
206
+
207
+ 插件向 pi 查询,不自行假设。`ctx.isIdle()` 回答是否还有运行、压缩或排队中的续跑,
208
+ `ctx.hasPendingMessages()` 回答输入是否已经在路上;探针缺失或抛错一律读作 `running`。空闲声明通常
209
+ 落在 `agent_settled`,因为 pi 只在一次运行彻底结算之后才触发它;单轮 turn 结束是另一件事。每
210
+ 10 秒的心跳每次触发都重新向 pi 推导阶段,因此重新开始的运行不会留下过期的空闲读数。
211
+
212
+ 后台任务扩展改变了这个问题。`bg_run` 与同类工具立即返回,工作继续在子进程里跑,于是 pi 在任务
213
+ 仍在进行时就等待输入。插件通过该扩展注册的工具认出它,再向它的 EventBus 服务查存活任务列表;
214
+ 处于 `running` 的任务会把 session 按在 `running` 上,也让阶梯退后,直到该任务报出终态。没有装
215
+ 这个扩展的 session 没有工具可认、没有查询,也没有东西要等。
216
+
217
+ 阶梯算的是一次空闲期,不是任务的一生。session 自己跑起来的任何一轮都把计数清零,于是恢复运行、
218
+ 继续干活之后,上限重新开始;阶梯自己的提醒唤醒的那一轮属于该提醒所属的空闲期,上限依然能达到。
181
219
 
182
220
  ## 5. 接力守卫
183
221
 
@@ -224,7 +262,7 @@ stderr 告警并忽略,把机会让回文件。
224
262
  | 作用域 | 本会话自己的投递,仅进程内存:重连不丢,会话重启从空开始,不去猜上一个进程发过什么 |
225
263
  | 豁免 | `force: true` 加非空 `reason`;只在守卫拒绝时才起作用 |
226
264
  | 审计 | 被豁免的 completion,ledger head 以 `relay-guard-forced: <reason>` 开头;调用带了 `text` 时紧接其后 |
227
- | 不管的路 | 自动终态:`agent_settled` 与 `recycle{outcome}` 照旧结算欠着接力的任务 |
265
+ | 不管的路 | 插件不经过模型就报出的终态:turn 出错、idle 阶梯用尽,以及 `recycle{outcome}` |
228
266
 
229
267
  `relay.toml` 是 TOML 的封闭子集:扁平的 `key = value` 行、上面两个键、单行双引号字符串数组、
230
268
  `#` 注释。子集之外一律 stderr 告警并忽略。它刻意不放 `.onlyne/config.toml`:client 以
@@ -238,17 +276,19 @@ stderr 告警并忽略,把机会让回文件。
238
276
 
239
277
  - **report 序号基址。** 插件自己的 `report` 序号从 1000 起,不是 1。client 把自身的派发事件
240
278
  (`created`、资源 attach、`ready`)写进同一个 `(generation, seq)` 水位,reducer 会静默丢弃
241
- 水位及以下的报告(`crates/onlyne-session/src/reconcile.rs`),所以从 1 起会丢掉最初的观测。
279
+ 水位及以下的报告(`crates/onlyne-session/src/reconcile/`),所以从 1 起会丢掉最初的观测。
242
280
  其余版本语义与规范一致。
243
281
  - **`observed` 是完整的 `Observation`。** `report.heartbeat` 携带整个合法状态元组
244
282
  (`version`、`generation_live`、`isolate_after`、`terminate_after`、`mismatch_count`、
245
- `agent`、`delivery`、`resource`、`recovery`、`outcome`、`public`),不是
246
- `{"state": "running"}` 这种简写。宿主会反序列化它,`is_legal` 不接受的一律拒绝。本插件只管
247
- `agent` 这一维(turn hooks),`delivery` 保持 `none`、`outcome` 保持 `pending`——在它报出
248
- completion 之前这就是它的事实。`resource` 报 `attached`,因为宿主的派发路径已经记过这次
249
- attach。
283
+ `agent`、`delivery`、`resource`、`recovery`),不是
284
+ `{"state": "running"}` 这种简写。宿主会反序列化它,先改写 client 主张的那六项,再套用 `is_legal`。本插件主张的
285
+ 是 `agent`(turn hooks)、`resource`(进程还活在那块 pane 里,attach 早被宿主派发路径记过)
286
+ 与 `host` 绑定这三件事。`delivery`、`recovery`、`generation_live`、`isolate_after`、
287
+ `terminate_after`、`mismatch_count` 它一个见证都没有:client 在归约前会用自己
288
+ 的 intent 队列、reducer 历史与角色配置重写这六项,插件填什么都不会被读。任务的 outcome 与公开视图
289
+ 更已经不在元组里,所以也就不再上报。
250
290
  - **`ready` 每连接报一次。** 宿主的 hand-off 路径
251
- (`crates/onlyne-client/src/dispatch.rs::hand_session`)在把 session 交给挂载的插件时已经报过
291
+ (`crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session`)在把 session 交给挂载的插件时已经报过
252
292
  `ready`,所以插件再报一次在宿主侧是 no-op。插件仍然发送:先挂载、后有活正是 ready barrier
253
293
  描述的情形,而且只花一帧。
254
294
  - **从不发 `cluster_ref`。** 本插件代表本地 role 说话,从不代表 aggregate;Rust 侧出于同样的
@@ -309,12 +349,12 @@ stderr 告警并忽略,把机会让回文件。
309
349
  | 反复 `reconnecting in 4000ms` | client 已停或 socket 被替换 | `onlyne --server-root … roles` |
310
350
  | `ready refused: internal: unknown session for …` | 插件为 client 从未暂存的任务报了 ready(手工起 pi 时的正常现象) | 让 client 拉起 pi,而不是手工起 |
311
351
  | `assign` 一直不来 | client 的 `session_command` 没能拉起 pi,或 `inject` 被降级 | client 日志里的 spawn 行;`/onlyne status` 看能力集 |
312
- | ledger 停在 `in_flight` | 没有 completion:没跑 turn,或 `agent_settled` 没触发 | pi session 文件里的 `onlyne-assign` / `onlyne-complete` 条目 |
352
+ | ledger 停在 `in_flight` | 还没有 completion:没跑过 turn(注入的消息尚未执行),或阶梯还在提醒(`idleReminders`) | pi session 文件里的 `onlyne-assign` 条目和其后注入的提醒;`onlyne` 面板里的 `reminder n of m`;`/onlyne status` 看任务与阶段 |
313
353
  | `onlyne_complete` 回答 `relay guard: missing handoff to: …` | 工作区的 spec(或顶替它的 `relay.toml`)点名了一个本会话从未触达的 role | 日常通知显示在 `onlyne` 面板;stderr 保留 `relay guard from …` 等拒绝、socket 错误、超时与帧错误;`required=…` 说明策略;`relay guard: missing handoff …` 列出已投递集合 |
314
354
  | `hello` 后立刻 `forbidden` / 断连 | mount role 与 client 的 role 不一致 | `hello.args.mount.role` 对该工作区的 role |
315
355
  | `frame_too_large` | 正文超过 8 MiB | 只会由超限的出站图片触发;上限来自核心 |
316
356
  | 工具缺失 | 该 pi 版本没有 `pi.registerTool` | `/onlyne status`;对照上面的能力表 |
317
- | 会话在 `exited` 之后又回到 `idle` | completion 之后还落进了一条 heartbeat 快照,带着 `outcome: pending` | 看 session 日志里 `completion` 之后的 report 顺序;插件对已完成任务不再上报 |
357
+ | 后台任务还在跑,会话却显示 `idle` | 没装后台任务扩展,或它的 EventBus 服务没在时限内回答状态查询,插件看不到自己留下的运行中工作 | `[pi-onlyne]` 日志里后台探针那一行;同一个 pi session 里的 `bg_status` 会列出存活任务 |
318
358
  | supervisor 看板一个 tab 都不列 | 没有 live session 上报过 pane:适配器版本早于这条上报,或这个 pi 不在 Orca pane 里 | `onlyne --server-root … sessions --json` 看 `projection.observed.host.orca.pane_key`;在 pane 里跑 `env \| grep ORCA_` |
319
359
 
320
360
  `/onlyne status` 打印实时状态(`connected`、`socket`、`role`、`sessionId`、`generation`、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onlyne",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Onlyne agent adapter for pi: the session lifecycle an onlyne role client expects from a pi host.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,6 +65,7 @@ test("a real onlyne-client answers hello with a welcome", { skip: !hasBinaries }
65
65
  status: () => {},
66
66
  welcome: () => {},
67
67
  isIdle: () => true,
68
+ waitingForInput: async () => true,
68
69
  exit: () => {},
69
70
  };
70
71
  const logs = [];