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.md +377 -43
- package/README.zh.md +73 -33
- package/package.json +1 -1
- package/src/agent.live.test.mjs +1 -0
- package/src/agent.mjs +233 -97
- package/src/agent.test.mjs +360 -86
- package/src/background-work.mjs +176 -0
- package/src/background-work.test.mjs +128 -0
- package/src/config.mjs +27 -5
- package/src/index.ts +42 -3
- package/src/pi-surface.mjs +40 -0
- package/src/protocol.mjs +41 -49
- package/src/protocol.test.mjs +58 -8
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ length-prefixed JSON codec, and the runtime has no npm dependencies.
|
|
|
9
9
|
|
|
10
10
|
Outside an onlyne session the extension is inert. The client injects `ONLYNE_ROLE`,
|
|
11
11
|
`ONLYNE_SESSION_ID` and `ONLYNE_TASK_ID` into every process it spawns
|
|
12
|
-
(`crates/onlyne-client/src/dispatch.rs`). With any of the three missing, this is an
|
|
12
|
+
(`crates/onlyne-client/src/session/dispatch.rs`). With any of the three missing, this is an
|
|
13
13
|
ordinary pi session: the plugin registers nothing and opens nothing.
|
|
14
14
|
|
|
15
15
|
```
|
|
@@ -24,10 +24,13 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
|
|
|
24
24
|
◀── assign{envelope, prose, task_id, generation}
|
|
25
25
|
├─ task text (+ image path) ──► pi user message (deliverAs:"followUp")
|
|
26
26
|
├─ assign_ack{accepted:true}
|
|
27
|
-
├─ report.heartbeat{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│
|
|
27
|
+
├─ report.heartbeat{agent} — `running` per turn and every 10s while a task is live,
|
|
28
|
+
│ `idle` only while pi waits for input; every beat re-derives it from pi
|
|
29
|
+
├─ the session waits for input with the task open and no `onlyne_complete` ──►
|
|
30
|
+
│ the idle ladder: the same message again (at most `idleReminders` times),
|
|
31
|
+
│ then `failed` and out
|
|
32
|
+
├─ report.complete{outcome, head} — the ledger's terminal fact, and the last report
|
|
33
|
+
│ └─ the client's answer is the handover: pi is asked to shut down, then detaches
|
|
31
34
|
├─ probe ──► one heartbeat
|
|
32
35
|
◀── recycle ──► complete (if unsettled) → stop → pi exits
|
|
33
36
|
└─ detach{reason} when pi shuts down
|
|
@@ -102,9 +105,10 @@ pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
|
|
|
102
105
|
| --- | --- | --- |
|
|
103
106
|
| `enabled` | `true` | `false` turns the extension off for this workspace |
|
|
104
107
|
| `watch.autoStart` | `true` | `false` registers the tools but opens no socket until `/onlyne connect` |
|
|
108
|
+
| `idleReminders` | `2` | how many times one idle episode re-sends the assignment before the task fails (§4); 0 means the first idle without a completion fails it |
|
|
105
109
|
|
|
106
|
-
A missing file means
|
|
107
|
-
keeps
|
|
110
|
+
A missing file means every default. A malformed file prints one warning on stderr and
|
|
111
|
+
keeps the defaults: a typo must not silently disable a role. The client does not read
|
|
108
112
|
this file (§11 of the plan downgraded the old readiness gates to generate-time template
|
|
109
113
|
advice), so only this extension consumes it; the key shape stays the one the templates
|
|
110
114
|
carry.
|
|
@@ -154,7 +158,8 @@ png/jpeg/gif/webp file: the plugin reads it, base64-encodes it and attaches it a
|
|
|
154
158
|
|
|
155
159
|
### `onlyne_complete{outcome?, text?, force?, reason?}`
|
|
156
160
|
|
|
157
|
-
Ends the current task with an explicit outcome (`done` by default, or `failed`).
|
|
161
|
+
Ends the current task with an explicit outcome (`done` by default, or `failed`). It is the
|
|
162
|
+
only path to `done`: a turn that ends without it is reminded and then failed (§4). A
|
|
158
163
|
non-empty `text` becomes the ledger `head` verbatim: whitespace collapses to one line and
|
|
159
164
|
the text stops at 200 characters. An absent or blank `text` carries no summary, so the
|
|
160
165
|
completion falls back to the last assistant text. The call also ends the session's
|
|
@@ -163,28 +168,53 @@ pi to shut down through `ctx.shutdown()`. pi 0.85.1 has no tool-result `terminat
|
|
|
163
168
|
handling. When the workspace carries a relay policy (§5), `force: true` with a non-empty
|
|
164
169
|
`reason` is the deliberate way past a handoff the session still owes.
|
|
165
170
|
|
|
171
|
+
### `onlyne_handoff{to, text, image?}`
|
|
172
|
+
|
|
173
|
+
Hands this session's task on to the next hop of its family. The plugin sends one `handoff`
|
|
174
|
+
frame naming the task the session currently holds, and the host mints one child task for
|
|
175
|
+
`to` under it: the child names this task as its `parent_task`, sits one hop further along,
|
|
176
|
+
and carries the same family id, hop budget, origin, deadline and labels. The tool's result
|
|
177
|
+
names the child task id and its hop. A client refusal comes back as the tool's error,
|
|
178
|
+
verbatim. `image` is the same absolute png/jpeg/gif/webp path the send tool takes. An
|
|
179
|
+
assignment whose causality names a hop budget states the hop and the budget in its
|
|
180
|
+
injected header line. `onlyne_send{kind: "task"}` is the other way to reach a role: that
|
|
181
|
+
envelope starts a family of its own at hop 0.
|
|
182
|
+
|
|
166
183
|
## 4. Outcome rules
|
|
167
184
|
|
|
168
|
-
The plugin sends one completion per task,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
`onlyne_complete` is the only path to `done`. The plugin sends one completion per task,
|
|
186
|
+
at the first of these events:
|
|
187
|
+
|
|
188
|
+
1. **`onlyne_complete`** — the model gives an explicit outcome (`done` by default, or
|
|
189
|
+
`failed` / `cancelled`). A later completion for the same task is refused (not
|
|
190
|
+
re-reported). Its non-empty `text` is the head.
|
|
191
|
+
2. **An errored turn** — the turn ended with a provider error (`stopReason: "error"`).
|
|
192
|
+
That is proof on its own, so the plugin reports `failed` at once, with the error as
|
|
193
|
+
the head.
|
|
194
|
+
3. **The idle ladder** — a turn ended cleanly without a completion, pi is waiting for
|
|
195
|
+
input, and the task is still open. The plugin re-sends the assignment and counts the
|
|
196
|
+
rung. The idle that finds the bound `idleReminders` names already spent reports
|
|
197
|
+
`failed` — head `no completion after <n> idle reminders` — and the session exits the
|
|
198
|
+
way any completion makes it exit.
|
|
199
|
+
4. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
|
|
181
200
|
unsettled task with the host's outcome first, then stops and exits pi.
|
|
182
201
|
|
|
202
|
+
Two things settle nothing: a task that was assigned but whose turn has not run yet (the
|
|
203
|
+
injected message has not executed, so completing now would claim work that never
|
|
204
|
+
happened), and an idle the ladder still has a rung for. The ladder re-sends the
|
|
205
|
+
assignment the task arrived with — the same header, task text and attachment paths the
|
|
206
|
+
injection carried, under one line saying the previous turn ended without a completion —
|
|
207
|
+
and never the role prose, which is already in the session's context. Its images are not
|
|
208
|
+
re-attached: the paths travel as text, so the same bytes are not put into the context
|
|
209
|
+
twice. A new envelope for the same task restarts the count, and so does any turn of the
|
|
210
|
+
session's own (see the idle claim below).
|
|
211
|
+
|
|
183
212
|
`head` is a single line, capped at 200 characters; it matches what the client puts in
|
|
184
213
|
`out_head` and what the receipt carries. Each task has one source for it: the `text` of
|
|
185
|
-
the explicit `onlyne_complete` call when that call carried one,
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
the explicit `onlyne_complete` call when that call carried one, the error a failed turn
|
|
215
|
+
reported, or the ladder's own line. The last assistant text is the fallback for an
|
|
216
|
+
`onlyne_complete` call that carried no text at all — a sentence spoken after such a call
|
|
217
|
+
cannot replace what the call handed over, and nothing else reads it.
|
|
188
218
|
|
|
189
219
|
A reported completion ends the session's process. `report.complete` goes out as a request,
|
|
190
220
|
and the client answers it only after it has settled the session row, acked the delivery
|
|
@@ -193,13 +223,39 @@ outcome the socket could not carry is queued and flushed after the next `hello`,
|
|
|
193
223
|
flush's answer is the handover that ends the process. A completion the host refused leaves
|
|
194
224
|
the process running, so an exit never loses the task.
|
|
195
225
|
|
|
196
|
-
The
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
The completion is the plugin's last report, and a session that has completed one answers
|
|
227
|
+
no further beat and no `probe`. The terminal agent state is the client's own write: the
|
|
228
|
+
`detach` frame that follows retires the task-free session, and that path feeds
|
|
229
|
+
`AgentGone` and publishes the row (`retire_idle_locked`,
|
|
230
|
+
`crates/onlyne-client/src/session/dispatch/retire.rs`). A process on its way out states
|
|
231
|
+
no phase for itself.
|
|
232
|
+
|
|
233
|
+
### The idle claim
|
|
234
|
+
|
|
235
|
+
A session is idle only while it waits for user input. Every other moment reads
|
|
236
|
+
`running`: a turn in flight, a queued steering or follow-up message, a retry, a
|
|
237
|
+
compaction, and work a background-task extension took off the agent loop.
|
|
238
|
+
|
|
239
|
+
The plugin asks pi instead of assuming. `ctx.isIdle()` answers whether a run, a
|
|
240
|
+
compaction or a queued continuation is still open, `ctx.hasPendingMessages()` answers
|
|
241
|
+
whether input is already on its way, and a probe that is missing or throws reads as
|
|
242
|
+
`running`. `agent_settled` is where the claim normally lands, because pi fires that
|
|
243
|
+
event only after a run has fully settled; the end of one turn is a different moment, and
|
|
244
|
+
the 10-second beat re-derives the phase from pi on every tick, so a stale one cannot
|
|
245
|
+
survive a run that started again.
|
|
246
|
+
|
|
247
|
+
A background-task extension changes the question. `bg_run` and its siblings return at
|
|
248
|
+
once and the work continues in a child process, so pi waits for input while the session's
|
|
249
|
+
task is still in flight. The plugin recognises the extension by the tools it registered
|
|
250
|
+
and asks its EventBus service for the live task list; a task in `running` status holds
|
|
251
|
+
the session at `running` and holds the ladder off until that task reports a terminal
|
|
252
|
+
state. On a session without the extension there is no tool to recognise, no query, and
|
|
253
|
+
nothing to wait for.
|
|
254
|
+
|
|
255
|
+
The ladder counts one idle episode, not a task's whole life. Any turn of the session's
|
|
256
|
+
own zeroes the count, so work that resumed and ran again starts the bound over; the one
|
|
257
|
+
turn the ladder's own reminder wakes belongs to the episode that reminder belongs to,
|
|
258
|
+
and the bound stays reachable.
|
|
203
259
|
|
|
204
260
|
## 5. Relay guard
|
|
205
261
|
|
|
@@ -253,7 +309,7 @@ file its turn.
|
|
|
253
309
|
| scope | this session's own sends, in process memory: a reconnect keeps them, a restarted session starts empty rather than guessing at what an earlier process sent |
|
|
254
310
|
| waiver | `force: true` with a non-empty `reason`; it only matters when the guard refuses |
|
|
255
311
|
| audit | a waived completion's ledger head starts with `relay-guard-forced: <reason>`, followed by the model's `text` when the call carried one |
|
|
256
|
-
| not guarded | the
|
|
312
|
+
| not guarded | outcomes the plugin reports without the model: an errored turn, the idle ladder's failure, and `recycle{outcome}` |
|
|
257
313
|
|
|
258
314
|
`relay.toml` is a closed subset of TOML: flat `key = value` lines, the two keys above,
|
|
259
315
|
one-line arrays of double-quoted strings, `#` comments. Anything outside that warns on
|
|
@@ -271,18 +327,22 @@ the shipped client.
|
|
|
271
327
|
- **Report sequence base.** The plugin's own `report` sequence starts at 1000, not 1. The
|
|
272
328
|
client stamps its own dispatch events (`created`, resource attach, `ready`) into the
|
|
273
329
|
same `(generation, seq)` watermark, and the reducer silently drops any report at or
|
|
274
|
-
below it (`crates/onlyne-session/src/reconcile
|
|
330
|
+
below it (`crates/onlyne-session/src/reconcile/`). A plugin sequence starting at 1
|
|
275
331
|
would lose its first observations. Everything else about the versioning is per spec.
|
|
276
|
-
- **`observed` is a full `Observation`.** `report.heartbeat` carries the
|
|
332
|
+
- **`observed` is a full `Observation`.** `report.heartbeat` carries the state
|
|
277
333
|
tuple (`version`, `generation_live`, `isolate_after`, `terminate_after`,
|
|
278
|
-
`mismatch_count`, `agent`, `delivery`, `resource`, `recovery
|
|
279
|
-
a `{"state": "running"}` shorthand: the host deserialises it
|
|
280
|
-
`is_legal`
|
|
281
|
-
`
|
|
282
|
-
|
|
283
|
-
|
|
334
|
+
`mismatch_count`, `agent`, `delivery`, `resource`, `recovery`), not
|
|
335
|
+
a `{"state": "running"}` shorthand: the host deserialises it, overwrites the six
|
|
336
|
+
keys the client owns, and applies only a tuple `is_legal` accepts. This plugin owns the `agent` dimension (turn hooks), the
|
|
337
|
+
`resource` claim — its process is live in the pane the attach was recorded on —
|
|
338
|
+
and the `host` binding. It has no witness for `delivery`, `recovery`,
|
|
339
|
+
`generation_live`, `isolate_after`, `terminate_after` or `mismatch_count`: the
|
|
340
|
+
client rewrites all six from its own intent queue, reducer history and role
|
|
341
|
+
config before the tuple is applied, so whatever this plugin sends there is
|
|
342
|
+
never read. Neither a
|
|
343
|
+
task outcome nor a public view travels in a tuple at all.
|
|
284
344
|
- **`ready` is reported once per connection.** The host's own hand-off path
|
|
285
|
-
(`crates/onlyne-client/src/dispatch.rs::hand_session`) already reports `ready` when the
|
|
345
|
+
(`crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session`) already reports `ready` when the
|
|
286
346
|
client stages the session for a mounting plugin, so a second report from the plugin is a
|
|
287
347
|
no-op at the host. The plugin sends it anyway: a plugin that mounts *before* any work
|
|
288
348
|
exists is the case the ready barrier names, and it costs one frame.
|
|
@@ -354,12 +414,12 @@ path the client's daemon bound, read when the environment carried none, §8).
|
|
|
354
414
|
| `reconnecting in 4000ms` in a loop | the client is down or the socket was replaced | `onlyne --server-root … roles` |
|
|
355
415
|
| `ready refused: internal: unknown session for …` | the plugin mounted and reported for a task the client never staged (normal when pi is started by hand outside a task) | start pi under the client, not by hand |
|
|
356
416
|
| `assign` never arrives | the client's `session_command` did not spawn pi, or `inject` was dropped | the client log for the spawn line; `/onlyne status` for the capability set |
|
|
357
|
-
| ledger stays `in_flight` | no completion
|
|
417
|
+
| ledger stays `in_flight` | no completion yet: no turn has run (the injected message has not executed), or the ladder is still reminding it (`idleReminders`) | the pi session file for the `onlyne-assign` entry and the reminders injected after it; the `onlyne` panel for `reminder n of m`; `/onlyne status` for the task and phase |
|
|
358
418
|
| `onlyne_complete` answers `relay guard: missing handoff to: …` | the workspace's spec (or a `relay.toml` standing in for it) names a role this session never sent to | routine notices appear in the `onlyne` panel; stderr keeps refusals such as `relay guard from …`, socket errors, timeouts and framing faults; `required=…` names the policy; `relay guard: missing handoff …` names the delivered set |
|
|
359
419
|
| `hello … forbidden` / connection closed right after `hello` | the mount role does not match the client's role | `hello.args.mount.role` vs the workspace's role |
|
|
360
420
|
| `frame_too_large` | a body above 8 MiB | only reachable through an oversize outbound image; the ceiling is the core's |
|
|
361
421
|
| tools missing | `pi.registerTool` is absent in that pi version | `/onlyne status`; the capability table above |
|
|
362
|
-
| session reads `idle`
|
|
422
|
+
| session reads `idle` while a background task still runs | the background-task extension is absent, or its EventBus service did not answer the status query in time, so the plugin cannot see the work it left running | the `[pi-onlyne]` log line for the background probe; `bg_status` in that same pi session names the live task |
|
|
363
423
|
| the supervisor board lists no tabs | no live session reported a pane: the adapter predates the report, or this pi is not inside an Orca pane | `onlyne --server-root … sessions --json` for `projection.observed.host.orca.pane_key`; `env \| grep ORCA_` inside the pane |
|
|
364
424
|
|
|
365
425
|
`/onlyne status` prints the live state (`connected`, `socket`, `role`, `sessionId`,
|
|
@@ -386,3 +446,277 @@ ONLYNE_BACKEND=fake BIN_DIR=target/debug bash crates/onlyne-testkit/e2e/pi-live.
|
|
|
386
446
|
After sourcing the shared helpers, the case exports `ONLYNE_BACKEND=exec`, so the client
|
|
387
447
|
spawns pi itself with a stdin pipe it keeps open for the life of the session. The
|
|
388
448
|
agent's own output lands in `<ws>/.onlyne/logs/session-<task>.log`.
|
|
449
|
+
|
|
450
|
+
# 中文说明 / Chinese Translation
|
|
451
|
+
|
|
452
|
+
## pi-onlyne — onlyne 的 pi 代理适配器
|
|
453
|
+
|
|
454
|
+
此 pi 扩展让一个 pi 进程承载一个 onlyne 角色会话。它连接到 `<role workspace>/.onlyne/run/s`,使用 `crates/onlyne-adapter/PROTOCOL.md` 中的适配器协议,并按照 `hello → welcome → assign → work → complete → detach` 驱动会话。此处不运行 Rust 代码:协议基于 Node 的 `node:net` 重新实现,使用手写的四字节长度前缀 JSON 编解码器,运行时没有 npm 依赖。
|
|
455
|
+
|
|
456
|
+
在一个 onlyne 会话之外,扩展不会执行任何操作。客户端会向其启动的每个进程注入 `ONLYNE_ROLE`、`ONLYNE_SESSION_ID` 和 `ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`)。任一变量缺失时,这就是一个普通的 pi 会话:插件不注册任何内容,也不打开任何内容。
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
pi session (spawned by onlyne-client)
|
|
460
|
+
│ env: ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
|
|
461
|
+
│ .pi/onlyne.json: { "enabled": true, "watch": { "autoStart": true } }
|
|
462
|
+
▼
|
|
463
|
+
hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{role,session,task_id,pid}}
|
|
464
|
+
◀── welcome{role, prose, generation, server, host_capabilities}
|
|
465
|
+
├─ prose ──► pi context, once (custom message, no turn)
|
|
466
|
+
├─ report.ready ──► the barrier the task payload waits behind
|
|
467
|
+
◀── assign{envelope, prose, task_id, generation}
|
|
468
|
+
├─ task text (+ image path) ──► pi user message (deliverAs:"followUp")
|
|
469
|
+
├─ assign_ack{accepted:true}
|
|
470
|
+
├─ report.heartbeat{agent} — `running` per turn and every 10s while a task is live,
|
|
471
|
+
│ `idle` only while pi waits for input; every beat re-derives it from pi
|
|
472
|
+
├─ the session waits for input with the task open and no `onlyne_complete` ──►
|
|
473
|
+
│ the idle ladder: the same message again (at most `idleReminders` times),
|
|
474
|
+
│ then `failed` and out
|
|
475
|
+
├─ report.complete{outcome, head} — the ledger's terminal fact, and the last report
|
|
476
|
+
│ └─ the client's answer is the handover: pi is asked to shut down, then detaches
|
|
477
|
+
├─ probe ──► one heartbeat
|
|
478
|
+
◀── recycle ──► complete (if unsettled) → stop → pi exits
|
|
479
|
+
└─ detach{reason} when pi shuts down
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## 1. 安装
|
|
483
|
+
|
|
484
|
+
该插件是一个 pi 包:`package.json` 声明了 `pi.extensions: ["./src/index.ts"]`,因此 pi 会直接加载 TypeScript 源码,无需构建步骤。
|
|
485
|
+
|
|
486
|
+
### 使用生成的工作区(常规路径)
|
|
487
|
+
|
|
488
|
+
`onlyne server generate` 会将 `[server].agent_package` 复制到 `<ws>/.onlyne/agent/<pkg-name>/`,并将该包以相对于设置文件本身的路径写入 `.pi/settings.json`:`../.onlyne/agent/<pkg-name>`(`crates/onlyne-server/src/generate.rs`)。pi 0.85.1 仅加载这种写法。项目的 `packages` 路径相对于包含设置文件的目录(`<ws>/.pi`)解析,因此 `../` 形式可到达 `<ws>/.onlyne/agent/<pkg-name>`。裸的 `.onlyne/agent/<pkg-name>` 条目会解析到 `<ws>/.pi/.onlyne/agent/<pkg-name>`,并将该包列在列表中,但不会加载它。监管器会启动生成的工作区,扩展也会随其一同分发,无需进行全局安装。
|
|
489
|
+
|
|
490
|
+
```toml
|
|
491
|
+
# spec.toml
|
|
492
|
+
[server]
|
|
493
|
+
agent_package = "/abs/path/to/plugins/onlyne-agent-pi" # read once, at generate time
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
onlyne server generate --root <server-root> --out <dir>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
生成的 `.pi/settings.json` 随后会包含:
|
|
501
|
+
|
|
502
|
+
```json
|
|
503
|
+
{ "packages": ["../.onlyne/agent/onlyne-agent-pi"] }
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`pi list` 会在“项目包”下列出该条目。要验证实际加载情况,可以让复制后的 `index.ts` 抛出异常并观察失败。
|
|
507
|
+
|
|
508
|
+
### 手动安装(不使用生成器)
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
cp -R plugins/onlyne-agent-pi <ws>/.onlyne/agent/onlyne-agent-pi
|
|
512
|
+
printf '{"packages":["../.onlyne/agent/onlyne-agent-pi"]}\n' > <ws>/.pi/settings.json
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### 从 npm 安装
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
pi install npm:pi-onlyne # user-level: every pi process on this box loads it
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
已发布的 npm 包是 `pi-onlyne`;`pi install npm:pi-onlyne@<version>` 可以固定一个版本。此安装路径也会覆盖普通的交互式会话,在这些会话中扩展保持不活动状态(没有 `ONLYNE_ROLE`,因此没有适配器)。角色工作区无需全局安装即可使用面板:上面的文件级复制或 `onlyne server generate` 会将插件的作用域限定到承载角色的工作区。
|
|
522
|
+
|
|
523
|
+
### 一次性使用/测试
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### 开关文件
|
|
530
|
+
|
|
531
|
+
`<cwd>/.pi/onlyne.json`(参见 `onlyne.json.example`):
|
|
532
|
+
|
|
533
|
+
| 键 | 默认值 | 效果 |
|
|
534
|
+
| --- | --- | --- |
|
|
535
|
+
| `enabled` | `true` | `false` 会为此工作区关闭扩展 |
|
|
536
|
+
| `watch.autoStart` | `true` | `false` 会注册工具,但在 `/onlyne connect` 前不打开套接字 |
|
|
537
|
+
| `idleReminders` | `2` | 一次空闲期内重新发送任务分配信息的次数,随后任务失败(§4);`0` 表示第一次没有完成的空闲就会使任务失败 |
|
|
538
|
+
|
|
539
|
+
文件缺失时,所有项均使用默认值。文件格式错误时,会在 stderr 打印一条警告并保留默认值:拼写错误不能使角色在无提示的情况下停用。客户端不读取此文件(计划 §11 已将旧的就绪门控降级为生成时模板建议),因此只有此扩展会读取它;键结构仍采用模板所带的结构。
|
|
540
|
+
|
|
541
|
+
无需其他设置。工作区在 `spec.toml` 中的 `session_command` 已按任务启动 `pi`(`["pi", "--session-id", "{session}"]`),客户端也会注入此扩展所依赖的环境变量。
|
|
542
|
+
|
|
543
|
+
## 2. 能力
|
|
544
|
+
|
|
545
|
+
`hello` 帧声明此插件实际实现的功能:
|
|
546
|
+
|
|
547
|
+
| 能力 | 声明 | 此处的含义 |
|
|
548
|
+
| --- | --- | --- |
|
|
549
|
+
| `register` | 始终 | 在 `welcome` 之后发送 `session_register{session_id, task_id, generation, pid, title}` |
|
|
550
|
+
| `report` | 始终 | `report.ready` / `report.heartbeat` / `report.complete` |
|
|
551
|
+
| `inject` | 当 `pi.sendUserMessage` 存在时 | 任务正文通过 `assign` 到达,并作为 pi 用户消息注入 |
|
|
552
|
+
| `recycle` | 始终 | `recycle` 会在任务尚未确定最终状态时将其确定,然后停止插件并退出 pi |
|
|
553
|
+
|
|
554
|
+
pi API 缺失时会发生什么,以及主机随后如何处理:
|
|
555
|
+
|
|
556
|
+
| 缺口 | 检测方式 | 行为 |
|
|
557
|
+
| --- | --- | --- |
|
|
558
|
+
| 没有 `registerTool`(较旧的 pi) | 在 `session_start` 时探测 | 不注册任何工具;协议路径不受影响,`/onlyne status` 仍可使用 |
|
|
559
|
+
| 没有 `sendUserMessage` | 在 `session_start` 时探测 | 从能力列表中移除 `inject`,主机通过 `config_get{key:"stdin:<text>"}` 传递任务,插件通过仍然可用的通道注入该内容 |
|
|
560
|
+
| 没有 `sendMessage` | 探测 | 来自 `welcome` 的角色说明不会作为上下文注入;任务本身仍会送达 |
|
|
561
|
+
| 没有 `appendEntry` | 探测 | 不记录 `onlyne-assign` / `onlyne-complete` 会话条目 |
|
|
562
|
+
| 没有 `ui.setStatus` | 保护性检测 | 跳过页脚状态行 |
|
|
563
|
+
| 没有 `ui.setWidget` | 保护性检测 | 常规通知继续通过页脚状态行和 stderr 上的 `[pi-onlyne]` 行传递 |
|
|
564
|
+
| 没有 `ctx.shutdown` | 保护性检测 | `recycle` 和任务完成仍会确定任务的最终状态;进程会保持运行,等待操作员关闭 |
|
|
565
|
+
|
|
566
|
+
### 活动面板
|
|
567
|
+
|
|
568
|
+
当主机报告存在 UI(`ctx.hasUI` 在 TUI 和 RPC 模式下为 `true`,在 print 和 JSON 模式下为 `false`),且 `ctx.ui.setWidget` 可用时,常规 onlyne 通知会显示在编辑器上方、键为 `onlyne` 的面板中。页眉显示角色、连接状态、代次、当前任务 id 和阶段。其下最多显示六条事件,按从新到旧排列:入站帧使用 `<=`,出站帧使用 `=>`,警告使用 `!!`,状态变化使用 `..`,重复投递使用 `~~`。重复的相同事件会合并为一行,并带 `xN`;面板最多容纳八行,每行上限为 96 个单元,`session_shutdown` 会清空面板。
|
|
569
|
+
|
|
570
|
+
## 3. 工具
|
|
571
|
+
|
|
572
|
+
仅在 onlyne 会话内注册。
|
|
573
|
+
|
|
574
|
+
### `onlyne_send{to, text, kind?, image?}`
|
|
575
|
+
|
|
576
|
+
通过 `send` 帧发送一个信封。`kind: "note"`(默认值)是自由文本,不携带 `op_id`。`kind: "task"` 将工作移交给一个角色,因此携带 `o-<uuid>` 幂等键和新的 `causality.task`。`image` 是 `png/jpeg/gif/webp` 文件的绝对路径:插件读取该文件,进行 base64 编码,并将其作为 `body.image` 附加。核心将该文件限制为 2 MiB,并接受四种 mime 类型。
|
|
577
|
+
|
|
578
|
+
### `onlyne_complete{outcome?, text?, force?, reason?}`
|
|
579
|
+
|
|
580
|
+
以明确结果结束当前任务(默认为 `done`,或为 `failed`)。这是通向 `done` 的唯一路径:没有调用它的轮次会先收到提醒,随后失败(§4)。非空 `text` 会原样成为账本的 `head`:空白折叠为一行,文本在 200 个字符处截断。缺失或为空的 `text` 不携带摘要,因此完成时回退到最后一条助手文本。该调用也会结束会话进程:客户端确认完成报告后(见 §4),插件会通过 `ctx.shutdown()` 请求 pi 关闭。pi 0.85.1 没有工具结果的 `terminate` 处理。工作区带有中继策略时(§5),`force: true` 搭配非空 `reason`,可明确绕过会话仍需完成的交接。
|
|
581
|
+
|
|
582
|
+
### `onlyne_handoff{to, text, image?}`
|
|
583
|
+
|
|
584
|
+
将此会话的任务交予其族的下一个节点。插件发送一个 `handoff` 帧,指明会话当前持有的任务,主机随后在 `to` 之下创建一个子任务:子任务将本任务命名为其 `parent_task`,位置向后一跳,并携带相同的族 id、跳数预算、origin、deadline 和 labels。工具结果会给出子任务 id 及其跳数。客户端拒绝会作为工具错误原样返回。`image` 是发送工具所接受的同类绝对 `png/jpeg/gif/webp` 路径。causality 指明跳数预算的任务分配信息,会在其注入的页眉行中写明当前跳数与预算。`onlyne_send{kind: "task"}` 是到达角色的另一种方式:该信封在第 0 跳启动一个新族。
|
|
585
|
+
|
|
586
|
+
## 4. 结果规则
|
|
587
|
+
|
|
588
|
+
`onlyne_complete` 是通向 `done` 的唯一路径。插件为每个任务发送一次完成报告,在以下事件中第一个发生时发送:
|
|
589
|
+
|
|
590
|
+
1. **`onlyne_complete`**——模型给出明确结果(默认为 `done`,或为 `failed` / `cancelled`)。同一任务后续的完成调用会被拒绝,不会再次报告。其非空 `text` 就是 head。
|
|
591
|
+
2. **出错的轮次**——该轮次以模型提供方错误结束(`stopReason: "error"`)。这本身即可证明出错,因此插件立即报告 `failed`,并将错误作为 head。
|
|
592
|
+
3. **空闲阶梯**——某轮次正常结束但未完成,pi 正在等待输入,且任务仍处于打开状态。插件重新发送任务分配信息,并计入当前级数。当某次空闲发现 `idleReminders` 指定的上限已经用尽时,会报告 `failed`——head 为 `no completion after <n> idle reminders`——并以完成时相同的方式退出会话。
|
|
593
|
+
4. **`recycle{outcome}`**——主机正在拆除会话。插件先使用主机给出的结果确定尚未确定状态的任务,然后停止并退出 pi。
|
|
594
|
+
|
|
595
|
+
有两种情况不会确定最终状态:任务已经分配,但其轮次尚未运行,此时注入消息尚未执行,立即完成会声称完成了从未发生的工作;以及阶梯仍有下一级可走的空闲。阶梯重新发送任务最初到达时的分配信息,使用与注入时相同的页眉、任务文本和附件路径,并附上一行说明上一轮次结束时没有完成。阶梯不会重新发送角色说明,因为角色说明已存在于会话上下文中。其图像不会再次附加:路径以文本形式传递,因此相同的字节不会两次进入上下文。同任务的新信封会重新开始计数。
|
|
596
|
+
|
|
597
|
+
`head` 是单行文本,上限为 200 个字符;它与客户端写入 `out_head` 的内容以及回执携带的内容一致。每个任务的 `head` 只有一个来源:显式 `onlyne_complete` 调用携带的 `text`、失败轮次所报告的错误,或者阶梯自身的文本行。对于完全没有携带文本的 `onlyne_complete` 调用,最后一条助手文本作为回退;此类调用之后说出的句子不能替代调用已经交付的内容,也不会被其他任何内容读取。
|
|
598
|
+
|
|
599
|
+
已报告的完成会结束会话进程。`report.complete` 以请求形式发出;客户端仅在完成会话记录的处理、确认投递并写入 `Completion` 信封后才回复。插件在该回复到达时请求 pi 关闭。套接字无法传输的结果会进入队列,并在下一次 `hello` 之后刷新;该次刷新收到的回复即为结束进程的交接。主机拒绝的完成会让进程继续运行,因此进程退出不会导致任务丢失。
|
|
600
|
+
|
|
601
|
+
完成报告是插件发出的最后一份报告;已经完成任务的会话不再回应心跳,也不再回应 `probe`。终态的 agent 维度由客户端自己写入:随后到达的 `detach` 会退役这个已无任务的会话,该路径会喂入 `AgentGone` 并发布这一行(`retire_idle_locked`,`crates/onlyne-client/src/session/dispatch/retire.rs`)。正在离开的进程不为自己声明阶段。
|
|
602
|
+
|
|
603
|
+
### 空闲判定
|
|
604
|
+
|
|
605
|
+
只有会话正在等待用户输入时,它才是空闲的。其余时刻一律读作 `running`:正在运行的轮次、已排队的 steer 或 follow-up 消息、重试、压缩,以及被后台任务扩展移出 agent 循环的工作。
|
|
606
|
+
|
|
607
|
+
插件向 pi 查询,而不是自行假设。`ctx.isIdle()` 回答是否还有运行、压缩或排队中的续跑,`ctx.hasPendingMessages()` 回答输入是否已经在路上;缺失或抛错的探针一律读作 `running`。`agent_settled` 通常是空闲声明落地的时刻,因为 pi 只在一次运行彻底结算之后才触发该事件;单个轮次的结束是另一个时刻。每 10 秒的心跳会在每次触发时重新向 pi 推导阶段,因此重新开始的运行不会留下过期的空闲读数。
|
|
608
|
+
|
|
609
|
+
后台任务扩展改变了这个问题。`bg_run` 及其同类工具立即返回,工作继续在子进程中运行,于是 pi 在会话任务仍在进行时等待输入。插件通过该扩展注册的工具识别它,并向它的 EventBus 服务查询存活任务列表;处于 `running` 状态的任务会让会话保持 `running`,并让阶梯退后,直到该任务报告终态。没有安装该扩展的会话没有可识别的工具、没有查询,也就没有可等待的东西。
|
|
610
|
+
|
|
611
|
+
阶梯统计的是一次空闲期,而不是任务的整个生命。会话自己启动的任何一轮次都会把计数清零,因此恢复运行并继续工作之后,上限重新开始;阶梯自己的提醒唤醒的那一轮次属于该提醒所属的空闲期,上限依然可以达到。
|
|
612
|
+
|
|
613
|
+
## 5. 中继守卫
|
|
614
|
+
|
|
615
|
+
会话即使没有交接任何工作,也可能报告 `done`。中继守卫用于防止此类意外:一个 bench 会话叙述了自身的进度,在待办事项尚未处理时调用了 `onlyne_complete`,下游的 `writer` 一直等待一个从未发送的交接。中继守卫仅判定投递事实——某个角色是否收到消息——不会判断已发送文本的形式或质量。
|
|
616
|
+
|
|
617
|
+
策略位于插件的 `package.json` 旁边,因此会随生成工作区所加载的副本一同分发:生成工作区中为 `<ws>/.onlyne/agent/onlyne-agent-pi/relay.toml`,手动安装中为 `relay.toml`。
|
|
618
|
+
|
|
619
|
+
```toml
|
|
620
|
+
relay_required = ["writer"] # these roles must have received a handoff
|
|
621
|
+
relay_required_count = 2 # legacy alias of relay_count: this many distinct downstream roles
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
`relay_count` 是规范计数键。`relay_required_count` 是其旧版别名,也是 `relay.toml` 自身采用的拼写。列表键和计数键同时存在时,`relay_required` 优先。
|
|
625
|
+
|
|
626
|
+
策略应放在规范中,而不是供应商目录中。`onlyne generate --force` 会重写此包被供应到的副本,并连同其中的手写 `relay.toml` 一同处理,因此一个 `[[client]]` 条目只需声明一次策略,客户端就会将其注入所启动的每个会话进程:
|
|
627
|
+
|
|
628
|
+
```toml
|
|
629
|
+
[[client]]
|
|
630
|
+
role = "planner"
|
|
631
|
+
relay_count = 2 # this many distinct downstream roles
|
|
632
|
+
relay_required = ["writer"] # these roles must have received a handoff
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
来源的优先级为 `environment > relay.toml > none`:`ONLYNE_RELAY_REQUIRED`(列表,逗号分隔)和 `ONLYNE_RELAY_COUNT`(计数,十进制)是客户端根据上述条目填充的变量;只有环境变量完全未指明策略时,才读取 `package.json` 旁边的 `relay.toml`;两个来源均未指定策略时,不启用守卫。规范同时命名两个变量时,两者都会注入,因此列表仍然优先。手写 `relay.toml` 仍是手动安装的应急入口,适用于规范始终未声明策略的机器;被环境变量遮蔽的文件会被直接忽略。变量已设置但无法解析时,会在 stderr 上报告并忽略,此时由文件决定策略。
|
|
636
|
+
|
|
637
|
+
| | |
|
|
638
|
+
| --- | --- |
|
|
639
|
+
| 默认 | 两个来源均未指明策略:不启用守卫,完成路径沿用守卫加入前此插件随包提供的路径 |
|
|
640
|
+
| 证据 | 此会话自身成功执行的 `onlyne_send` 调用所触达的角色,包括 `note` 和 `task`;被拒绝的信封不计入任何角色 |
|
|
641
|
+
| 拒绝 | `onlyne_complete` 抛出 `onlyne: relay guard: missing handoff to: writer (…)`,并指出缺少的内容及解除方法 |
|
|
642
|
+
| 拒绝之后 | 不会报告任何内容,不会将结果排队,也不会分离:会话保持挂载状态;交接发出后,同一调用即可通过 |
|
|
643
|
+
| 列表模式 | 每个列出的角色都必须按字面出现在已送达集合中 |
|
|
644
|
+
| 计数模式 | 不同的下游角色;发送到当前角色自身或发回分配任务的角色不计入 |
|
|
645
|
+
| 范围 | 此会话自身在进程内存中的发送记录:重新连接会保留记录,重启后的会话从空状态开始,不推测早先进程发送过什么 |
|
|
646
|
+
| 豁免 | `force: true` 搭配非空 `reason`;仅在守卫拒绝时发挥作用 |
|
|
647
|
+
| 审计 | 获豁免的完成报告,其账本 head 以 `relay-guard-forced: <reason>` 开头;若调用携带了模型的 `text`,则将其附加在后面 |
|
|
648
|
+
| 不受守卫保护 | 插件在没有模型参与时报告的结果:出错的轮次、阶梯导致的失败,以及 `recycle{outcome}` |
|
|
649
|
+
|
|
650
|
+
`relay.toml` 是 TOML 的封闭子集:扁平的 `key = value` 行、上述两个键、由双引号字符串构成的单行数组,以及 `#` 注释。超出此范围的内容会在 stderr 上警告并被忽略。它不采用 `.onlyne/config.toml`:客户端使用 `deny_unknown_fields` 解析该文件,因此在其中加入插件键会阻止客户端启动。
|
|
651
|
+
|
|
652
|
+
没有生效策略时,`force` 和 `reason` 不起作用。
|
|
653
|
+
|
|
654
|
+
## 6. 协议说明与差异
|
|
655
|
+
|
|
656
|
+
下面每项都是对 `PROTOCOL.md` 的有意解读,或是在已发布客户端上测得的行为。
|
|
657
|
+
|
|
658
|
+
- **报告序列基线。** 插件自身的 `report` 序列从 1000 开始,而非 1。客户端将自己的调度事件(`created`、资源附加、`ready`)记入同一个 `(generation, seq)` 水位,归约器会静默丢弃任何小于或等于该水位的报告(`crates/onlyne-session/src/reconcile/`)。从 1 开始的插件序列会丢失最初几条观测。版本控制的其他部分均遵循规范。
|
|
659
|
+
- **`observed` 是一个完整的 `Observation`。** `report.heartbeat` 携带状态元组(`version`、`generation_live`、`isolate_after`、`terminate_after`、`mismatch_count`、`agent`、`delivery`、`resource`、`recovery`),而不是 `{"state": "running"}` 简写:主机对其进行反序列化,覆盖客户端拥有的六个键,并仅应用 `is_legal` 接受的元组。此插件拥有 `agent` 维度(轮次钩子)、`resource` 声明——其进程在记录了挂载操作的窗格中处于活动状态——以及 `host` 绑定。它没有 `delivery`、`recovery`、`generation_live`、`isolate_after`、`terminate_after` 或 `mismatch_count` 的观测依据:在应用元组之前,客户端会依据自己的意图队列、归约器历史和角色配置重写全部六项,因此此插件在这些位置发送的内容不会被读取。任务结果和公开视图均不通过元组传输。
|
|
660
|
+
- **`ready` 每次连接报告一次。** 主机自身的交接路径(`crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session`)已会在客户端为挂载插件暂存会话时报告 `ready`,因此主机会将插件的第二次报告视为空操作。插件仍会发送:在任何工作存在之前完成挂载的插件正是就绪屏障所涵盖的情况,而且该报告只占用一个帧。
|
|
661
|
+
- **从不发送 `cluster_ref`。** 此插件代表本地角色,不代表聚合角色;出于相同原因,Rust 一侧会将该字段设为 `skip_serializing_if`,使其缺省。
|
|
662
|
+
- **使用心跳响应 `probe`**,遵循 `PROTOCOL.md` 中“`probe` 声明新的资源观测”的说明。
|
|
663
|
+
- **仅当 `config_get` 以 `stdin:` 开头时,才将其读取为任务正文**,这是 `PROTOCOL.md` 为缺少 `inject` 的插件记录的重载。任何其他键都会记录到日志并被忽略,不会被误读。
|
|
664
|
+
- **`frame_too_large` / `bad_frame`**:正文过大时,会在写入任何字节之前拒绝;分帧错误会关闭连接并重新连接。正文损坏后,分帧无法重新同步,这也与 `crates/onlyne-frame/src/lib.rs` 得出的结论相同。
|
|
665
|
+
- **投递具备幂等性;任务不具备。** 去重键是信封 id。同一投递出现两次只会产生一次注入,以及带有 `reason: "duplicate"` 的确认;为已在运行的任务创建的新信封,会作为另一条消息到达该会话——工作记录保留其计数器和其中继账本,仅其“自该指令以来的轮次”看门狗重新启动。客户端为每个信封生成新的 uuid,因此 `duplicate` 仅会在真正重新提供任务时触发。
|
|
666
|
+
|
|
667
|
+
- **窗格绑定(Orca 标签页)。** 在 Orca 窗格内,插件会在每次心跳中通过报告 `Observation` 里的 `observed.host.orca.pane_key` 报告其运行所在的窗格(`crates/onlyne-session/src/host.rs`),并在环境变量指明时一并报告 `tab_id` / `leaf_id` 和终端 `handle`。绑定是从*内部*继承的,从不猜测:Orca 窗格会将其 `ORCA_PANE_KEY` / `ORCA_TAB_ID` / `ORCA_LEAF_ID` / `ORCA_TERMINAL_HANDLE` 导出到所启动的命令中(测于 2026-09-11,Orca 1.4.198),客户端会将自己的环境传递给会话命令。因此,窗格内的进程是唯一能够从内部说明 onlyne 会话位于哪个窗格的组件;pi 下游的任何内容都无法恢复此信息。在窗格之外,`host` 键会完全缺失:普通终端中的 pi 会报告不含 host 字段的观测,不会生成空窗格字段。
|
|
668
|
+
- **不会为此向工作区写入任何内容。** 已不再有绑定声明文件:绑定信息随客户端已经镜像的观测一同传递。不存在声明文件,因为没有组件创建它,工作区的缓存目录也不会被触及。这使 `integrations/orca-plugin` 无需读取任何路径即可将标签页轴限定为真实会话,也让监管器仍能说明一个*已结束*会话的运行位置:`report.complete` 会继续携带 `host`。
|
|
669
|
+
|
|
670
|
+
## 7. 配置参考
|
|
671
|
+
|
|
672
|
+
| 环境变量 | 是否必需 | 效果 |
|
|
673
|
+
| --- | --- | --- |
|
|
674
|
+
| `ONLYNE_ROLE` | 是 | 挂载角色 |
|
|
675
|
+
| `ONLYNE_SESSION_ID` | 是 | 挂载的会话 id;已发布客户端中的 `session_id` 等于 `task_id` |
|
|
676
|
+
| `ONLYNE_TASK_ID` | 是 | 此进程承载的任务;驱动 `session_register` 和初始的 `ready` |
|
|
677
|
+
| `ONLYNE_SOCKET` | 否 | 客户端为此工作区提供服务的套接字,会注入所启动的每个会话进程;变量未设置时,插件读取标记 `<cwd>/.onlyne/run/socket` 以获取守护进程公布的路径,并回退到 `<cwd>/.onlyne/run/s` |
|
|
678
|
+
| `ONLYNE_RELAY_REQUIRED` | 否 | 角色规范中的 `relay_required`,以逗号连接:守卫的列表模式(§5) |
|
|
679
|
+
| `ONLYNE_RELAY_COUNT` | 否 | 角色规范中的 `relay_count`:守卫的计数模式,仅在列表为空时决定结果(§5) |
|
|
680
|
+
| `ORCA_PANE_KEY` | 否 | 此进程的运行位置(`<tab_id>:<leaf_id>`),每次心跳通过 `observed.host.orca.pane_key` 上报;在 Orca 窗格之外未设置,因此该字段会缺失 |
|
|
681
|
+
| `ORCA_TAB_ID` / `ORCA_LEAF_ID` | 否 | 单独的窗格 id;仅设置窗格键本身时,会对该键进行解析 |
|
|
682
|
+
| `ORCA_TERMINAL_HANDLE` | 否 | 终端句柄,在窗格键旁以 `host.orca.handle` 上报,其值即 `orca terminal switch` 所使用的值 |
|
|
683
|
+
|
|
684
|
+
需要知道的常量:插件每 10 s 发送一次心跳(`heartbeat_timeout_ms` 为 30 s),为 `hello` 留出 5 s,每个请求留出 30 s,并按 1/2/4/8/16/30 s 的阶梯重新连接。
|
|
685
|
+
|
|
686
|
+
插件会读取自己的三个文件:`<cwd>/.pi/onlyne.json`(开关,§1)、`package.json` 旁边的 `relay.toml`(中继策略的回退来源,仅在客户端没有注入策略时读取,§5),以及 `<cwd>/.onlyne/run/socket`(标记客户端守护进程所绑定的套接字路径,当环境变量未携带该路径时读取,§8)。
|
|
687
|
+
|
|
688
|
+
## 8. 故障排除
|
|
689
|
+
|
|
690
|
+
| 症状 | 原因 | 检查 |
|
|
691
|
+
| --- | --- | --- |
|
|
692
|
+
| `[pi-onlyne] session …` 始终未出现 | 三个环境变量中缺少一个,或 `enabled` 为 false | `env \| grep ONLYNE_`;`cat .pi/onlyne.json` |
|
|
693
|
+
| `socket error: connect ENOENT …/.onlyne/run/s` | 此工作区没有运行 `onlyne-client run` | 启动客户端,或 `onlyne-client status` |
|
|
694
|
+
| 深层工作区出现 `socket error: connect EINVAL …/.onlyne/run/s` | macOS 为 `sun_path` 提供 104 字节,因此超过 103 的套接字路径会被拒绝;生成的角色工作区嵌套在服务器根目录下三层,过长的根路径会使规范拼写超过此上限。客户端会从临时目录下的短路径为此类工作区提供服务,并将其公布在 `<workspace>/.onlyne/run/socket` | 通过 `onlyne-client status` 查找 `onlyne: client running … socket <path>` 这一行,其中会指明所服务的路径;还需查看带有 `socket = <path>` 的客户端日志行;`cat <workspace>/.onlyne/run/socket` 包含同一路径,环境变量未注入任何值时,插件会连接该路径 |
|
|
695
|
+
| `reconnecting in 4000ms` 持续循环 | 客户端已停止,或套接字已被替换 | `onlyne --server-root … roles` |
|
|
696
|
+
| `ready refused: internal: unknown session for …` | 插件完成挂载并为一个客户端从未暂存的任务进行了报告(在任务之外手动启动 pi 时属于正常情况) | 在客户端下启动 pi,不手动启动 |
|
|
697
|
+
| `assign` 始终未到达 | 客户端的 `session_command` 未启动 pi,或 `inject` 已被移除 | 在客户端日志中查看启动行;通过 `/onlyne status` 查看能力集合 |
|
|
698
|
+
| 账本保持 `in_flight` | 尚未完成:还没有轮次运行(注入消息尚未执行),或者阶梯仍在提醒(`idleReminders`) | 在 pi 会话文件中查看 `onlyne-assign` 条目及其后注入的提醒;通过 `onlyne` 面板查看 `reminder n of m`;通过 `/onlyne status` 查看任务和阶段 |
|
|
699
|
+
| `onlyne_complete` 回复 `relay guard: missing handoff to: …` | 工作区的规范(或替代该规范的 `relay.toml`)指定了一个此会话从未向其发送的角色 | 常规通知会出现在 `onlyne` 面板中;stderr 会保留拒绝消息,例如 `relay guard from …`、套接字错误、超时和分帧错误;`required=…` 指明策略;`relay guard: missing handoff …` 指明已送达的集合 |
|
|
700
|
+
| `hello … forbidden`/在 `hello` 后立即关闭连接 | 挂载角色与客户端角色不匹配 | `hello.args.mount.role` 与工作区角色 |
|
|
701
|
+
| `frame_too_large` | 正文超过 8 MiB | 仅可通过超大的出站图像达到;此上限来自核心 |
|
|
702
|
+
| 工具缺失 | 该 pi 版本中不存在 `pi.registerTool` | `/onlyne status`;上方的能力表 |
|
|
703
|
+
| 后台任务仍在运行时,会话显示 `idle` | 未安装后台任务扩展,或其 EventBus 服务未在时限内回应状态查询,插件看不到自己留下的运行中工作 | `[pi-onlyne]` 日志中关于后台探针的行;同一 pi 会话中的 `bg_status` 会列出存活任务 |
|
|
704
|
+
| 监管器面板未列出任何标签页 | 没有活动会话报告窗格:适配器早于该报告功能,或此 pi 不在 Orca 窗格内 | `onlyne --server-root … sessions --json` 中的 `projection.observed.host.orca.pane_key`;在窗格内执行 `env \| grep ORCA_` |
|
|
705
|
+
|
|
706
|
+
`/onlyne status` 会打印实时状态(`connected`、`socket`、`role`、`sessionId`、`generation`、`agentState`、`tasks`、`pendingCompletion`、`lastError`、计数器),`/onlyne connect` / `/onlyne disconnect` 可手动打开和关闭套接字。
|
|
707
|
+
|
|
708
|
+
## 9. 开发
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
cd plugins/onlyne-agent-pi
|
|
712
|
+
node --test src/*.test.mjs # framing, protocol, agent state machine, config, relay guard, socket path
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
除非 `target/debug/onlyne-client` 和 `onlyne-server` 存在,否则 `src/agent.live.test.mjs` 会自行跳过。`crates/onlyne-testkit/e2e/pi-live.sh` 是端到端用例:pi 缺失或没有可用的模型凭据时,它会跳过(退出码 0);否则,它会通过真实客户端运行一个真实任务,直到 `acked`。
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
cd ../..
|
|
719
|
+
ONLYNE_BACKEND=fake BIN_DIR=target/debug bash crates/onlyne-testkit/e2e/pi-live.sh
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
该用例加载共享辅助函数后,会导出 `ONLYNE_BACKEND=exec`,因此客户端会自行启动 pi,并使用在整个会话期间保持打开的 stdin 管道。代理自身的输出会写入 `<ws>/.onlyne/logs/session-<task>.log`。
|