pi-onlyne 2.1.0 → 2.3.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 +77 -35
- package/README.zh.md +48 -19
- package/package.json +1 -1
- package/src/activity.mjs +294 -0
- package/src/activity.test.mjs +69 -1
- package/src/agent.mjs +157 -84
- package/src/agent.test.mjs +97 -45
- package/src/config.mjs +173 -3
- package/src/config.test.mjs +37 -1
- package/src/index.ts +163 -18
- package/src/pi-surface.mjs +69 -8
- package/src/pi-surface.test.mjs +78 -4
- package/src/session-scope.mjs +90 -0
- package/src/session-scope.test.mjs +61 -0
- package/src/socket.mjs +45 -0
- package/src/socket.test.mjs +27 -0
package/README.md
CHANGED
|
@@ -14,6 +14,15 @@ Outside an onlyne session the extension is inert. The client injects `ONLYNE_ROL
|
|
|
14
14
|
(`crates/onlyne-client/src/session/dispatch.rs`). With any of the three missing, this is an
|
|
15
15
|
ordinary pi session: the plugin registers nothing and opens nothing.
|
|
16
16
|
|
|
17
|
+
Only the session whose own id equals `ONLYNE_SESSION_ID` mounts. A pi process runs more
|
|
18
|
+
than one session — a `task` subagent's sidechain, `/new` — and each of them loads this
|
|
19
|
+
extension with the same inherited environment, so a session that is not the bound one
|
|
20
|
+
stays inert and every onlyne tool it calls is refused. In resident mode (`ONLYNE_ROLE` or
|
|
21
|
+
`.pi/onlyne.json` `"resident": true` / `.onlyne/config.toml` `placement = "external"`), the
|
|
22
|
+
extension also tracks top-level `session_switch`, `session_branch`, and `session_tree`
|
|
23
|
+
events so `/resume`, `/new`, and context rollover keep the tools bound to the active
|
|
24
|
+
top-level session.
|
|
25
|
+
|
|
17
26
|
```
|
|
18
27
|
pi session (spawned by onlyne-client)
|
|
19
28
|
│ env: ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
|
|
@@ -25,14 +34,17 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
|
|
|
25
34
|
├─ report.ready ──► the barrier the task payload waits behind
|
|
26
35
|
◀── assign{envelope, prose, text, attachments, task_id, generation}
|
|
27
36
|
├─ prose, when the welcome did not already deliver it ──► the same section
|
|
28
|
-
├─ text ──►
|
|
29
|
-
│
|
|
37
|
+
├─ text ──► one `onlyne_assign` card (`From <sender> · <kind> · <id>`), byte for byte:
|
|
38
|
+
│ `triggerTurn` starts the turn an idle session owes it and `deliverAs:"followUp"` queues
|
|
39
|
+
│ while busy, so the delivery is one message and never also a user prompt; a pi without
|
|
40
|
+
│ `sendMessage` takes it as a plain user message instead. `body.image` rides as a pi image
|
|
41
|
+
│ part, and `attachments` names files the client wrote
|
|
30
42
|
├─ assign_ack{accepted:true}
|
|
31
43
|
├─ report.heartbeat{agent} — `running` per turn and every 10s while a task is live,
|
|
32
44
|
│ `idle` only while pi waits for input; every beat re-derives it from pi
|
|
33
45
|
├─ the client owns the turn-end rule: a turn that ends without a completion
|
|
34
46
|
│ earns one nudge, and the second such ending settles the delivery
|
|
35
|
-
◀── nudge{task_id, text} ──►
|
|
47
|
+
◀── nudge{task_id, text} ──► the same card route, byte for byte (the client's sentence)
|
|
36
48
|
├─ report.complete{outcome, head, details, files} — the ledger's terminal fact, and the last report
|
|
37
49
|
│ └─ the client's answer is the handover: pi is asked to shut down, then detaches
|
|
38
50
|
├─ probe ──► one heartbeat
|
|
@@ -145,7 +157,7 @@ What happens when a pi API is missing, and what the host does then:
|
|
|
145
157
|
|
|
146
158
|
### Activity panel
|
|
147
159
|
|
|
148
|
-
When the host reports a UI (`ctx.hasUI`, true in the TUI and RPC modes, false in print and JSON modes) and `ctx.ui.setWidget` is available, routine onlyne notices draw in the panel above the editor with widget key `onlyne`. The header shows role, connection state, generation, the current task id, and phase. Below it, up to six newest-first events use `<=` for inbound frames, `=>` for outbound frames, `!!` for warnings, `..` for state changes, and `~~` for duplicate deliveries. Repeated identical events fold into one line with `xN
|
|
160
|
+
When the host reports a UI (`ctx.hasUI`, true in the TUI and RPC modes, false in print and JSON modes) and `ctx.ui.setWidget` is available, routine onlyne notices draw in the panel above the editor with widget key `onlyne`. The widget registers a dual-surface component: on ANSI terminals it renders theme-styled lines (`accent` role badge, status-toned connection state, dimmed timestamps, and semantic event markers), and on Tern Surface Protocol (TSP) hosts it implements `describe()` to emit a native collapsible `card` (`role: "omp.widget.onlyne"`, `variant: "bare"`) with structured spans. An inbound delivery (`assign` and `nudge`) travels as the visible `onlyne_assign` custom message card (`From <sender> · <kind> · <id>`) that `registerMessageRenderer("onlyne_assign", ...)` draws: it reaches the model as that one message, so a delivery is never both drawn twice and read twice. `wakeUser` passes `{ triggerTurn: true }` when `ctx.isIdle()` is true, which is what starts the turn an empty or settled resident session owes the card (and `{ deliverAs: "followUp" }` while busy). The header shows role, connection state, generation, the current task id, and phase. Below it, up to six newest-first events use `<=` for inbound frames, `=>` for outbound frames, `!!` for warnings, `..` for state changes, and `~~` for duplicate deliveries. Repeated identical events fold into one line with `xN` (and pre-connection resident polling stays quiet until a socket connects); the panel holds at most eight lines, each capped at 96 cells, and `session_shutdown` clears it.
|
|
149
161
|
|
|
150
162
|
## 3. Tools
|
|
151
163
|
|
|
@@ -157,10 +169,20 @@ Each result is one plain sentence — the recipient for a send or a handoff, the
|
|
|
157
169
|
for a completion. A tool result is model-visible, and nothing about the plugin's own
|
|
158
170
|
bookkeeping belongs in it.
|
|
159
171
|
|
|
172
|
+
The three tools belong to the session that mounted them. A pi process runs more than one
|
|
173
|
+
session — a `task` subagent's sidechain, `/new` — and every one of them loads this
|
|
174
|
+
extension with the same inherited environment, so a call from a session whose id is not
|
|
175
|
+
`ONLYNE_SESSION_ID` is refused with a sentence naming both ids instead of settling the
|
|
176
|
+
parent's task. A pi that does not expose a session id (there is no `getSessionId` in
|
|
177
|
+
`ctx.sessionManager`) is allowed through: the env-bound mount is still the only one that
|
|
178
|
+
connected.
|
|
179
|
+
|
|
160
180
|
### `onlyne_send{to, text, kind?, image?}`
|
|
161
181
|
|
|
162
|
-
Sends one envelope on the `send` frame. `kind: "note"` (the default)
|
|
163
|
-
|
|
182
|
+
Sends one envelope on the `send` frame. `kind: "note"` (the default) sends stateless
|
|
183
|
+
conversational text to a working, idle, or parked resident session, carries no `op_id`,
|
|
184
|
+
opens no task record, and requires no `onlyne_complete`. `kind: "task"` hands tracked work
|
|
185
|
+
to a role, so it carries an `o-<uuid>`
|
|
164
186
|
idempotency key and a fresh `causality.task`. `image` is an absolute path to a
|
|
165
187
|
png/jpeg/gif/webp file: the plugin reads it, base64-encodes it and attaches it as
|
|
166
188
|
`body.image`. The core caps that at 2 MiB and accepts four mime types. The result is
|
|
@@ -178,7 +200,10 @@ receive, with the client holding the ceiling and refusing an oversize body with
|
|
|
178
200
|
sentence (§3c of the contract). The call also ends the session's process: once the client
|
|
179
201
|
has acknowledged the completion report (§4), the plugin asks pi to shut down through
|
|
180
202
|
`ctx.shutdown()`. pi 0.85.1 has no tool-result `terminate` handling. The result is
|
|
181
|
-
`reported <outcome
|
|
203
|
+
`reported <outcome>` — or, when a client that had already settled the task answers the
|
|
204
|
+
report with its own record, `task already settled as <state> at <settled_at>: <head> —
|
|
205
|
+
this call changed nothing`, which is the one case where the call did not cause the
|
|
206
|
+
outcome it asked for.
|
|
182
207
|
|
|
183
208
|
### `onlyne_handoff{to, text, image?}`
|
|
184
209
|
|
|
@@ -196,33 +221,36 @@ of its own at hop 0.
|
|
|
196
221
|
|
|
197
222
|
## 4. Outcome rules
|
|
198
223
|
|
|
199
|
-
`onlyne_complete` is the only path to `done
|
|
200
|
-
at the first of these events:
|
|
224
|
+
`onlyne_complete` is the only path to `done`, and it is the plugin's one completion path.
|
|
225
|
+
The plugin sends one completion per task, at the first of these events:
|
|
201
226
|
|
|
202
227
|
1. **`onlyne_complete`** — the model gives an explicit outcome: `done`, `failed`,
|
|
203
228
|
`cancelled` or `blocked`. A later completion for the same task is refused (not
|
|
204
229
|
re-reported). Its non-empty `summary` is the head.
|
|
205
|
-
2.
|
|
206
|
-
That is proof on its own, so the plugin reports `failed` at once, with the error as
|
|
207
|
-
the head. The report itself waits until pi is waiting for input, so the beat that
|
|
208
|
-
carries it states a phase the session really has.
|
|
209
|
-
3. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
|
|
230
|
+
2. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
|
|
210
231
|
unsettled task with the host's outcome first, then stops and exits pi.
|
|
211
232
|
|
|
212
|
-
A turn that ends
|
|
213
|
-
|
|
214
|
-
own sentence as a `nudge`, and decides what a
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
A turn that ends without a completion settles nothing here, and neither does a turn that
|
|
234
|
+
ends with one. The endings belong to the client (`docs/v2-CONTRACT.md`, "3c. One
|
|
235
|
+
turn-end rule"): it counts them, sends its own sentence as a `nudge`, and decides what a
|
|
236
|
+
delivery that never reports becomes; its own sweeps own a session whose agent died. The
|
|
237
|
+
plugin hands the nudge to pi, answers only that it did, and reports only its phase and
|
|
238
|
+
the completion the model asked for — one owner for the wording, the count and the
|
|
239
|
+
settlement, rather than two. A task whose injected message has not run a turn is left
|
|
240
|
+
alone whatever a settle signal says: reporting now would claim work that never happened.
|
|
241
|
+
|
|
242
|
+
A turn that ended with a provider error (`stopReason: "error"`) is not a verdict either.
|
|
243
|
+
pi retries a retryable provider error and stays inside the same run, and the client's
|
|
244
|
+
nudge can restart a session a moment after it settles, so a report sent on the error
|
|
245
|
+
would race work that is still going. The error only supplies the head fallback described
|
|
246
|
+
below.
|
|
219
247
|
|
|
220
248
|
`head` is a single line, capped at 200 characters; it matches what the client puts in
|
|
221
249
|
`out_head` and what the receipt carries. Each task has one source for it: the `summary`
|
|
222
|
-
of the explicit `onlyne_complete` call when that call carried one
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
of the explicit `onlyne_complete` call when that call carried one. A call whose `summary`
|
|
251
|
+
is empty falls back to what the session last said — the last assistant text, or the error
|
|
252
|
+
of a turn that failed. The error is only ever that fallback: it is never a completion, and
|
|
253
|
+
a sentence spoken after such a call cannot replace what the call handed over.
|
|
226
254
|
|
|
227
255
|
A reported completion ends the session's process. `report.complete` goes out as a request,
|
|
228
256
|
and the client answers it only after it has settled the session row, acked the delivery
|
|
@@ -231,6 +259,13 @@ outcome the socket could not carry is queued and flushed after the next `hello`,
|
|
|
231
259
|
flush's answer is the handover that ends the process. A completion the host refused leaves
|
|
232
260
|
the process running, so an exit never loses the task.
|
|
233
261
|
|
|
262
|
+
A client that had already settled this task answers the same request with
|
|
263
|
+
`{"already_settled": {state, settled_at, head}}` instead of `null`. The plugin hands that
|
|
264
|
+
record on rather than claiming an outcome it did not cause, and the tool result says so
|
|
265
|
+
(§3); on the reconnect flush, where no tool result exists to carry it, the record reaches
|
|
266
|
+
the `[pi-onlyne]` log. Either way the task stays settled: the client's row is the
|
|
267
|
+
authority.
|
|
268
|
+
|
|
234
269
|
The completion is the plugin's last report, and a session that has completed one answers
|
|
235
270
|
no further beat and no `probe`. The terminal agent state is the client's own write: the
|
|
236
271
|
`detach` frame that follows retires the task-free session, and that path feeds
|
|
@@ -256,9 +291,9 @@ A background-task extension changes the question. `bg_run` and its siblings retu
|
|
|
256
291
|
once and the work continues in a child process, so pi waits for input while the session's
|
|
257
292
|
task is still in flight. The plugin recognises the extension by the tools it registered
|
|
258
293
|
and asks its EventBus service for the live task list; a task in `running` status holds
|
|
259
|
-
the session at `running`,
|
|
260
|
-
|
|
261
|
-
to recognise, no query, and nothing to wait for.
|
|
294
|
+
the session at `running`, so the beat states a phase the session really has. No report of
|
|
295
|
+
the plugin's own waits on that: nothing is reported at a turn end any more. On a session
|
|
296
|
+
without the extension there is no tool to recognise, no query, and nothing to wait for.
|
|
262
297
|
|
|
263
298
|
## 5. Protocol notes and deviations
|
|
264
299
|
|
|
@@ -403,6 +438,8 @@ agent's own output lands in `<ws>/.onlyne/logs/session-<task>.log`.
|
|
|
403
438
|
|
|
404
439
|
在一个 onlyne 会话之外,扩展不会执行任何操作。客户端会向其启动的每个进程注入 `ONLYNE_ROLE`、`ONLYNE_SESSION_ID` 和 `ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`)。任一变量缺失时,这就是一个普通的 pi 会话:插件不注册任何内容,也不打开任何内容。
|
|
405
440
|
|
|
441
|
+
只有自身会话 id 等于 `ONLYNE_SESSION_ID` 的会话才会挂载。一个 pi 进程会跑不止一个会话——`task` 子代理的 sidechain、`/new`——它们都带着同一份继承来的环境变量加载本扩展,因此不是被绑定会话的那个会话保持静默,它发出的每一次 onlyne 工具调用都会被拒绝。
|
|
442
|
+
|
|
406
443
|
```
|
|
407
444
|
pi session (spawned by onlyne-client)
|
|
408
445
|
│ env: ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
|
|
@@ -522,6 +559,8 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
|
|
|
522
559
|
|
|
523
560
|
每个结果只有一句平实的话——send 与 handoff 给出发往的角色,complete 给出结果。工具结果是模型可见的,插件自己的簿记没有理由出现在里面。
|
|
524
561
|
|
|
562
|
+
这三个工具属于挂载它们的那个会话。一个 pi 进程会跑不止一个会话——`task` 子代理的 sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;因此,当调用的会话 id 不等于 `ONLYNE_SESSION_ID` 时,调用会被拒绝,并给出同时点名两个 id 的一句话,而不是去结算父任务。若该 pi 不暴露会话 id(`ctx.sessionManager` 没有 `getSessionId`),调用照常放行:此时唯一连上 socket 的仍然是那个由环境变量绑定的挂载。
|
|
563
|
+
|
|
525
564
|
### `onlyne_send{to, text, kind?, image?}`
|
|
526
565
|
|
|
527
566
|
通过 `send` 帧发送一个信封。`kind: "note"`(默认值)是自由文本,不携带 `op_id`。`kind: "task"` 将工作移交给一个角色,因此携带 `o-<uuid>` 幂等键和新的 `causality.task`。`image` 是 `png/jpeg/gif/webp` 文件的绝对路径:插件读取该文件,进行 base64 编码,并将其作为 `body.image` 附加。核心将该文件限制为 2 MiB,并接受四种 mime 类型。
|
|
@@ -530,7 +569,7 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
|
|
|
530
569
|
|
|
531
570
|
### `onlyne_complete{outcome, summary, details?, files?}`
|
|
532
571
|
|
|
533
|
-
以明确结果结束当前任务——即 proto 的 `Outcome`:`done`、`failed`、`cancelled` 或 `blocked`。`summary` 是展示用的一行,原样成为账本的 `head`:空白折叠为一行,文本在 200 个字符处截断;`summary` 为空时不携带展示行,head 回退到最后一条助手文本。`details` 是完整结果,`files` 是它所点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,上限由客户端把守,超限正文由客户端用自己的句子拒绝(契约 §3c)。该调用也会结束会话进程:客户端确认完成报告后(见 §4),插件会通过 `ctx.shutdown()` 请求 pi 关闭。pi 0.85.1 没有工具结果的 `terminate` 处理。结果为 `reported <outcome
|
|
572
|
+
以明确结果结束当前任务——即 proto 的 `Outcome`:`done`、`failed`、`cancelled` 或 `blocked`。`summary` 是展示用的一行,原样成为账本的 `head`:空白折叠为一行,文本在 200 个字符处截断;`summary` 为空时不携带展示行,head 回退到最后一条助手文本。`details` 是完整结果,`files` 是它所点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,上限由客户端把守,超限正文由客户端用自己的句子拒绝(契约 §3c)。该调用也会结束会话进程:客户端确认完成报告后(见 §4),插件会通过 `ctx.shutdown()` 请求 pi 关闭。pi 0.85.1 没有工具结果的 `terminate` 处理。结果为 `reported <outcome>`;若客户端此前已经结算过该任务,它会用自己的记录作答,此时结果变成 `task already settled as <state> at <settled_at>: <head> — this call changed nothing`——这是调用并未造成它所请求的那个结果的唯一情形。
|
|
534
573
|
|
|
535
574
|
### `onlyne_handoff{to, text, image?}`
|
|
536
575
|
|
|
@@ -538,18 +577,21 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
|
|
|
538
577
|
|
|
539
578
|
## 4. 结果规则
|
|
540
579
|
|
|
541
|
-
`onlyne_complete` 是通向 `done`
|
|
580
|
+
`onlyne_complete` 是通向 `done` 的唯一路径,也是插件唯一的完成路径。插件为每个任务发送一次完成报告,在以下事件中第一个发生时发送:
|
|
542
581
|
|
|
543
582
|
1. **`onlyne_complete`**——模型给出明确结果:`done`、`failed`、`cancelled` 或 `blocked`。同一任务后续的完成调用会被拒绝,不会再次报告。其非空 `summary` 就是 head。
|
|
544
|
-
2.
|
|
545
|
-
|
|
583
|
+
2. **`recycle{outcome}`**——主机正在拆除会话。插件先使用主机给出的结果确定尚未确定状态的任务,然后停止并退出 pi。
|
|
584
|
+
|
|
585
|
+
没有完成就结束的轮次在这里不结算任何东西,以错误结束的轮次同样不结算。结束这件事归 client(`docs/v2-CONTRACT.md` 的「3c. One turn-end rule」):它统计这类结束、把自己的一句话作为 `nudge` 发来,并决定一次始终不上报的投递会变成什么;agent 已死的会话由它自己的清扫负责。插件把那句话交给 pi,并且只回答自己交出去了,同时只上报自己的阶段与模型主动要求的完成——措辞、计数与结算都只有一个拥有者,而不是两个。注入消息尚未跑过任何轮次的任务,无论结算信号说什么都不动:现在就上报等于宣称从未发生的工作已经完成。
|
|
546
586
|
|
|
547
|
-
|
|
587
|
+
以提供方错误(`stopReason: "error"`)结束的轮次也不是结论:pi 会对可重试的提供方错误重试,并留在同一次运行里,而 client 的 nudge 可能在会话结算之后的一瞬间重新启动它,所以在这里发出报告只会与仍在进行的工作赛跑。错误只提供下面说的 head 回退。
|
|
548
588
|
|
|
549
|
-
`head` 是单行文本,上限为 200 个字符;它与客户端写入 `out_head` 的内容以及回执携带的内容一致。每个任务的 `head` 只有一个来源:显式 `onlyne_complete` 调用携带 `summary`
|
|
589
|
+
`head` 是单行文本,上限为 200 个字符;它与客户端写入 `out_head` 的内容以及回执携带的内容一致。每个任务的 `head` 只有一个来源:显式 `onlyne_complete` 调用携带 `summary` 时就是它。完全没有携带 `summary` 的调用回退到会话最后说过的话——最后一条助手文本,或失败轮次留下的错误。错误永远只是这个回退:它本身从不构成完成,而此类调用之后说出的句子不能替代调用已经交付的内容。
|
|
550
590
|
|
|
551
591
|
已报告的完成会结束会话进程。`report.complete` 以请求形式发出;客户端仅在完成会话记录的处理、确认投递并写入 `Completion` 信封后才回复。插件在该回复到达时请求 pi 关闭。套接字无法传输的结果会进入队列,并在下一次 `hello` 之后刷新;该次刷新收到的回复即为结束进程的交接。主机拒绝的完成会让进程继续运行,因此进程退出不会导致任务丢失。
|
|
552
592
|
|
|
593
|
+
若客户端此前已经结算过该任务,它会对同一个请求回答 `{"already_settled": {state, settled_at, head}}` 而不是 `null`。插件把这个记录原样转交,而不是声称一个并非由本次调用造成的结果,工具结果也照此说明(见 §3);在重连补发那条路径上,没有工具结果可以承载它,记录会进入 `[pi-onlyne]` 日志。无论哪种情形任务都保持已结算:以客户端的那一行为准。
|
|
594
|
+
|
|
553
595
|
完成报告是插件发出的最后一份报告;已经完成任务的会话不再回应心跳,也不再回应 `probe`。终态的 agent 维度由客户端自己写入:随后到达的 `detach` 会退役这个已无任务的会话,该路径会喂入 `AgentGone` 并发布这一行(`retire_idle_locked`,`crates/onlyne-client/src/session/dispatch/retire.rs`)。正在离开的进程不为自己声明阶段。
|
|
554
596
|
|
|
555
597
|
### 空闲判定
|
|
@@ -558,7 +600,7 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
|
|
|
558
600
|
|
|
559
601
|
插件向 pi 查询,而不是自行假设。`ctx.isIdle()` 回答是否还有运行、压缩或排队中的续跑,`ctx.hasPendingMessages()` 回答输入是否已经在路上;缺失或抛错的探针一律读作 `running`。`agent_settled` 通常是空闲声明落地的时刻,因为 pi 只在一次运行彻底结算之后才触发该事件;单个轮次的结束是另一个时刻。每 10 秒的心跳会在每次触发时重新向 pi 推导阶段,因此重新开始的运行不会留下过期的空闲读数。
|
|
560
602
|
|
|
561
|
-
后台任务扩展改变了这个问题。`bg_run` 及其同类工具立即返回,工作继续在子进程中运行,于是 pi 在会话任务仍在进行时等待输入。插件通过该扩展注册的工具识别它,并向它的 EventBus 服务查询存活任务列表;处于 `running` 状态的任务会让会话保持 `running
|
|
603
|
+
后台任务扩展改变了这个问题。`bg_run` 及其同类工具立即返回,工作继续在子进程中运行,于是 pi 在会话任务仍在进行时等待输入。插件通过该扩展注册的工具识别它,并向它的 EventBus 服务查询存活任务列表;处于 `running` 状态的任务会让会话保持 `running`,于是心跳陈述的是会话真实的阶段。插件自己没有任何报告要等它结束:轮次结束时已经不再上报任何东西。没有安装该扩展的会话没有可识别的工具、没有查询,也就没有可等待的东西。
|
|
562
604
|
|
|
563
605
|
## 5. 协议说明与差异
|
|
564
606
|
|
package/README.zh.md
CHANGED
|
@@ -10,6 +10,13 @@ socket——机器级运行目录里的 `<digest>.sock`(`/tmp/onlyne-<uid>/`
|
|
|
10
10
|
`ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`);三者缺一,就是普通 pi session,
|
|
11
11
|
插件不注册任何工具、不打开任何 socket。
|
|
12
12
|
|
|
13
|
+
只有自身 session id 等于 `ONLYNE_SESSION_ID` 的那个 session 才会挂载。一个 pi 进程会跑不止一个
|
|
14
|
+
session——`task` 子代理的 sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;
|
|
15
|
+
因此不是被绑定 session 的那个保持静默,它发出的每一次 onlyne 工具调用都会被拒绝。在常驻模式
|
|
16
|
+
(`ONLYNE_ROLE`、`.pi/onlyne.json` `"resident": true` 或 `.onlyne/config.toml`
|
|
17
|
+
`placement = "external"`)下,扩展同时监听顶层 `session_switch`、`session_branch` 与
|
|
18
|
+
`session_tree` 事件,使 `/resume`、`/new` 及上下文切换后工具自动跟随当前顶层会话。
|
|
19
|
+
|
|
13
20
|
```
|
|
14
21
|
pi session(由 onlyne-client spawn)
|
|
15
22
|
│ 环境变量:ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
|
|
@@ -21,8 +28,11 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
|
|
|
21
28
|
├─ report.ready ──► 载荷等待的那道 barrier
|
|
22
29
|
◀── assign{envelope, prose, text, attachments, task_id, generation}
|
|
23
30
|
├─ prose(仅当 welcome 尚未投递过) ──► 同一个 section
|
|
24
|
-
├─ text ──►
|
|
25
|
-
│
|
|
31
|
+
├─ text ──► 一张 `onlyne_assign` 卡片(`From <sender> · <kind> · <id>`),逐字节原样:
|
|
32
|
+
│ `triggerTurn` 让空闲会话就此起一个 turn,繁忙时改 `deliverAs:"followUp"` 排队,
|
|
33
|
+
│ 于是投递只有这一条消息、不会同时再是一条 user prompt;没有 `sendMessage` 的 pi 退回
|
|
34
|
+
│ 普通 user message。`body.image` 作为 pi image part 同行,`attachments` 里的路径是
|
|
35
|
+
│ client 已写好的文件
|
|
26
36
|
├─ assign_ack{accepted:true}
|
|
27
37
|
├─ report.heartbeat{agent} —— 每个 turn 以及任务存续期间每 10 秒发 `running`,
|
|
28
38
|
│ 只有 pi 等待输入时才发 `idle`;每次心跳都重新向 pi 推导
|
|
@@ -135,7 +145,7 @@ role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为
|
|
|
135
145
|
|
|
136
146
|
### 活动面板
|
|
137
147
|
|
|
138
|
-
宿主报告有 UI 时(`ctx.hasUI`:TUI 与 RPC 模式为 true,print 与 JSON 模式为 false)且 `ctx.ui.setWidget` 可用,日常 onlyne 通知显示在编辑器上方,widget key 为 `onlyne
|
|
148
|
+
宿主报告有 UI 时(`ctx.hasUI`:TUI 与 RPC 模式为 true,print 与 JSON 模式为 false)且 `ctx.ui.setWidget` 可用,日常 onlyne 通知显示在编辑器上方,widget key 为 `onlyne`。小组件支持 ANSI 与 Tern Surface Protocol(TSP)双渲染面:在 ANSI 终端下使用主题令牌为标题、连接状态、时间戳和方向标记着色排版;在支持 TSP 的 Tern 终端下通过 `describe()` 输出原生可折叠 `card` 组件(`role: "omp.widget.onlyne"`,`variant: "bare"`)与语义化 span。入站投递(`assign` 与 `nudge`)走的就是那张带 `From <sender> · <kind> · <id>` 抬头的 `onlyne_assign` 卡片:`wakeUser` 用 `pi.sendMessage({ customType: "onlyne_assign", display: true, details })` 发出它,`registerMessageRenderer("onlyne_assign", ...)` 画它,模型读到的也正是这一条——投递不会被画两遍又读两遍。`ctx.isIdle()` 为真时 `wakeUser` 传 `{ triggerTurn: true }`,把空闲或已结算的常驻会话欠这张卡片的那个 turn 起起来;繁忙时改传 `{ deliverAs: "followUp" }` 排队。标题行显示 role、连接状态、generation、当前 task id 与阶段。其下最多六条事件,按最新在前排列:`<=` 入站,`=>` 出站,`!!` 警告,`..` 状态,`~~` 重复投递。连续相同事件折成一行并带 `xN`(常驻会话在客户端首次连通前的后台探测保持静默,不刷屏 `reconnecting`);面板最多八行,每行最多 96 个显示单元,`session_shutdown` 时清除。
|
|
139
149
|
|
|
140
150
|
## 3. 工具面
|
|
141
151
|
|
|
@@ -145,9 +155,16 @@ role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为
|
|
|
145
155
|
每个结果只有一句话——send 与 handoff 说发给谁,complete 说结果。工具结果模型看得见,
|
|
146
156
|
插件自己的账本没有理由出现在里面。
|
|
147
157
|
|
|
158
|
+
这三个工具属于挂载它们的那个 session。一个 pi 进程会跑不止一个 session——`task` 子代理的
|
|
159
|
+
sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;因此当调用的 session id
|
|
160
|
+
不等于 `ONLYNE_SESSION_ID` 时,调用会被拒绝,并给出同时点名两个 id 的一句话,而不是去结算父
|
|
161
|
+
任务。若该 pi 不暴露 session id(`ctx.sessionManager` 没有 `getSessionId`),调用照常放行:
|
|
162
|
+
此时唯一连上 socket 的仍然是那个由环境变量绑定的挂载。
|
|
163
|
+
|
|
148
164
|
### `onlyne_send{to, text, kind?, image?}`
|
|
149
165
|
|
|
150
|
-
经 `send` 帧提交一个 envelope。`kind: "note"
|
|
166
|
+
经 `send` 帧提交一个 envelope。`kind: "note"`(默认)向运行中、空闲或常驻待命的会话发送无状态交流消息,
|
|
167
|
+
不带 `op_id`,不创建任务记录,也不要求调用 `onlyne_complete`。
|
|
151
168
|
`kind: "task"` 是派人办事,因此带 `o-<uuid>` 幂等键和新生成的 `causality.task`。`image` 是
|
|
152
169
|
png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成 `body.image`。核心限 2 MiB,
|
|
153
170
|
只收四种 mime。结果是 `sent to <role>`。
|
|
@@ -160,7 +177,10 @@ png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成
|
|
|
160
177
|
`files` 是它点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,
|
|
161
178
|
上限由 client 把守,超限正文由 client 用自己的句子拒绝(契约 §3c)。这一调用同时结束所在
|
|
162
179
|
session 的进程。client 应答完 completion 报告(见 §4)之后,插件通过 `ctx.shutdown()` 让 pi
|
|
163
|
-
退出。pi 0.85.1 没有 tool-result `terminate` 处理。结果是 `reported <outcome
|
|
180
|
+
退出。pi 0.85.1 没有 tool-result `terminate` 处理。结果是 `reported <outcome>`;若 client 此前
|
|
181
|
+
已经结算过该任务,它会用自己的记录作答,此时结果变成 `task already settled as <state> at
|
|
182
|
+
<settled_at>: <head> — this call changed nothing`——这是调用并未造成它所请求的那个结果的唯一
|
|
183
|
+
情形。
|
|
164
184
|
|
|
165
185
|
### `onlyne_handoff{to, text, image?}`
|
|
166
186
|
|
|
@@ -175,32 +195,40 @@ hop 预算留在 envelope 的 `causality` 里,属于协议数据:投递文
|
|
|
175
195
|
|
|
176
196
|
## 4. outcome 判定规则
|
|
177
197
|
|
|
178
|
-
`onlyne_complete` 是通向 `done`
|
|
198
|
+
`onlyne_complete` 是通向 `done` 的唯一路径,也是插件唯一的完成路径。插件每个任务只发一次
|
|
199
|
+
completion,取以下两者的先到者:
|
|
179
200
|
|
|
180
201
|
1. **`onlyne_complete`** —— 模型给显式 outcome:`done`、`failed`、`cancelled` 或 `blocked`。同一
|
|
181
202
|
任务的第二次 completion 被拒(不重报)。`summary` 非空时即 head,原样写出。
|
|
182
|
-
2.
|
|
183
|
-
报 `failed`,错误信息当 head。上报本身要等到 pi 正在等待输入时才发出,所以承载它的那次心跳
|
|
184
|
-
陈述的是 session 真实的阶段。
|
|
185
|
-
3. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
|
|
203
|
+
2. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
|
|
186
204
|
插件并退出 pi。
|
|
187
205
|
|
|
188
|
-
|
|
206
|
+
没有 completion 就结束的 turn 在这里不结算任何东西,以错误结束的 turn 同样不结算。结束这件事归 client
|
|
189
207
|
(`docs/v2-CONTRACT.md` 的 “3c. One turn-end rule”):它数这类结束、把自己的一句话作为 `nudge`
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
208
|
+
发来,并决定一次始终不上报的投递会变成什么;agent 已死的 session 由它自己的清扫负责。插件把那
|
|
209
|
+
句话交给 pi,并且只回答自己交出去了,同时只上报自己的阶段与模型主动要求的 completion——措辞、
|
|
210
|
+
计数与结算都只有一个主人,而不是两个。注入的消息还没跑过任何 turn 的任务,无论结算信号说什么都
|
|
211
|
+
不动:现在就报终态,等于声称干过一件没发生过的活。
|
|
212
|
+
|
|
213
|
+
以 provider 错误(`stopReason: "error"`)结束的 turn 也不是结论:pi 会对可重试的 provider 错误
|
|
214
|
+
重试,并留在同一次 run 里,而 client 的 nudge 可能在 session 结算之后的一瞬重新启动它,所以在
|
|
215
|
+
这里发报告只会与仍在进行的工作赛跑。错误只提供下面说的 head 退路。
|
|
193
216
|
|
|
194
217
|
`head` 恒为单行、上限 200 字符,与 client 写入 `out_head` 和回执携带的内容一致。每个任务的 head
|
|
195
|
-
只有一个来源:显式 `onlyne_complete` 带 `summary`
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
只有一个来源:显式 `onlyne_complete` 带 `summary` 时就是它。完全没有带 `summary` 的调用退到
|
|
219
|
+
session 最后说过的话——最后一段 assistant 文本,或失败 turn 留下的错误。错误永远只是这个退路:
|
|
220
|
+
它本身从不构成 completion,此外没有任何东西读它;而调用之后再说的话顶不掉调用交出的内容。
|
|
198
221
|
|
|
199
222
|
报出去的 completion 会结束所在 session 的进程。`report.complete` 以请求形式发出,client 只有
|
|
200
223
|
在结算 session 行、ack 掉投递、并写好 `Completion` envelope 之后才应答,插件就在这个应答处
|
|
201
224
|
让 pi 退出。socket 当时送不出去的 outcome 会被记住,并在下一次 `hello` 后补发,那次补发的
|
|
202
225
|
应答就是结束进程的交接点。被宿主拒掉的 completion 不会让进程退出,任务不会因为退出而丢失。
|
|
203
226
|
|
|
227
|
+
若 client 此前已经结算过该任务,它会对同一个请求回答 `{"already_settled": {state, settled_at,
|
|
228
|
+
head}}` 而不是 `null`。插件把这个记录原样转交,而不是声称一个并非由本次调用造成的结果,工具结果
|
|
229
|
+
也照此说明(见 §3);在重连补发那条路径上没有工具结果可以承载它,记录会进入 `[pi-onlyne]` 日志。
|
|
230
|
+
无论哪种情形任务都保持已结算:以 client 的那一行为准。
|
|
231
|
+
|
|
204
232
|
completion 是插件最后一份上报;已经交掉任务的 session 不再发心跳,也不再应答 `probe`。终态的
|
|
205
233
|
agent 维度由 client 自己写:随后到达的 `detach` 会退役这个已无任务的 session,该路径会喂入
|
|
206
234
|
`AgentGone` 并发布这一行(`retire_idle_locked`,
|
|
@@ -218,8 +246,9 @@ steer 或 follow-up 消息、重试、压缩,以及被后台任务扩展移出
|
|
|
218
246
|
|
|
219
247
|
后台任务扩展改变了这个问题。`bg_run` 与同类工具立即返回,工作继续在子进程里跑,于是 pi 在任务
|
|
220
248
|
仍在进行时就等待输入。插件通过该扩展注册的工具认出它,再向它的 EventBus 服务查存活任务列表;
|
|
221
|
-
处于 `running` 的任务会把 session 按在 `running`
|
|
222
|
-
|
|
249
|
+
处于 `running` 的任务会把 session 按在 `running` 上,于是心跳陈述的是 session 真实的阶段。插件
|
|
250
|
+
自己没有任何报告要等它结束:轮次结束时已经不再上报任何东西。没有装这个扩展的 session 没有工具
|
|
251
|
+
可认、没有查询,也没有东西要等。
|
|
223
252
|
|
|
224
253
|
## 5. 协议说明与偏差
|
|
225
254
|
|