pi-onlyne 1.1.1 → 1.2.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 +75 -41
- package/README.zh.md +54 -30
- package/package.json +1 -1
- package/src/agent.mjs +175 -49
- package/src/agent.test.mjs +245 -31
- package/src/config.mjs +27 -5
- package/src/index.ts +39 -7
- package/src/protocol.mjs +41 -49
- package/src/protocol.test.mjs +58 -8
- package/src/socket.mjs +54 -0
- package/src/socket.test.mjs +79 -0
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
|
```
|
|
@@ -25,8 +25,10 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
|
|
|
25
25
|
├─ task text (+ image path) ──► pi user message (deliverAs:"followUp")
|
|
26
26
|
├─ assign_ack{accepted:true}
|
|
27
27
|
├─ report.heartbeat{running|idle} — per turn, and every 10s while a task is live
|
|
28
|
+
├─ a turn that ends without `onlyne_complete` ──► the idle ladder: the same
|
|
29
|
+
│ message again (at most `idleReminders` times), then `failed` and out
|
|
28
30
|
├─ report.complete{outcome, head} — the ledger's terminal fact
|
|
29
|
-
│ └─ then one report.heartbeat{agent:"idle"}
|
|
31
|
+
│ └─ then one report.heartbeat{agent:"idle"} stating the agent only
|
|
30
32
|
│ └─ the client's answer is the handover: pi is asked to shut down, then detaches
|
|
31
33
|
├─ probe ──► one heartbeat
|
|
32
34
|
◀── recycle ──► complete (if unsettled) → stop → pi exits
|
|
@@ -102,9 +104,10 @@ pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
|
|
|
102
104
|
| --- | --- | --- |
|
|
103
105
|
| `enabled` | `true` | `false` turns the extension off for this workspace |
|
|
104
106
|
| `watch.autoStart` | `true` | `false` registers the tools but opens no socket until `/onlyne connect` |
|
|
107
|
+
| `idleReminders` | `2` | how many times an idle turn end re-sends the assignment before the task fails (§4); 0 means the first idle without a completion fails it |
|
|
105
108
|
|
|
106
|
-
A missing file means
|
|
107
|
-
keeps
|
|
109
|
+
A missing file means every default. A malformed file prints one warning on stderr and
|
|
110
|
+
keeps the defaults: a typo must not silently disable a role. The client does not read
|
|
108
111
|
this file (§11 of the plan downgraded the old readiness gates to generate-time template
|
|
109
112
|
advice), so only this extension consumes it; the key shape stays the one the templates
|
|
110
113
|
carry.
|
|
@@ -154,7 +157,8 @@ png/jpeg/gif/webp file: the plugin reads it, base64-encodes it and attaches it a
|
|
|
154
157
|
|
|
155
158
|
### `onlyne_complete{outcome?, text?, force?, reason?}`
|
|
156
159
|
|
|
157
|
-
Ends the current task with an explicit outcome (`done` by default, or `failed`).
|
|
160
|
+
Ends the current task with an explicit outcome (`done` by default, or `failed`). It is the
|
|
161
|
+
only path to `done`: a turn that ends without it is reminded and then failed (§4). A
|
|
158
162
|
non-empty `text` becomes the ledger `head` verbatim: whitespace collapses to one line and
|
|
159
163
|
the text stops at 200 characters. An absent or blank `text` carries no summary, so the
|
|
160
164
|
completion falls back to the last assistant text. The call also ends the session's
|
|
@@ -163,28 +167,52 @@ pi to shut down through `ctx.shutdown()`. pi 0.85.1 has no tool-result `terminat
|
|
|
163
167
|
handling. When the workspace carries a relay policy (§5), `force: true` with a non-empty
|
|
164
168
|
`reason` is the deliberate way past a handoff the session still owes.
|
|
165
169
|
|
|
170
|
+
### `onlyne_handoff{to, text, image?}`
|
|
171
|
+
|
|
172
|
+
Hands this session's task on to the next hop of its family. The plugin sends one `handoff`
|
|
173
|
+
frame naming the task the session currently holds, and the host mints one child task for
|
|
174
|
+
`to` under it: the child names this task as its `parent_task`, sits one hop further along,
|
|
175
|
+
and carries the same family id, hop budget, origin, deadline and labels. The tool's result
|
|
176
|
+
names the child task id and its hop. A client refusal comes back as the tool's error,
|
|
177
|
+
verbatim. `image` is the same absolute png/jpeg/gif/webp path the send tool takes. An
|
|
178
|
+
assignment whose causality names a hop budget states the hop and the budget in its
|
|
179
|
+
injected header line. `onlyne_send{kind: "task"}` is the other way to reach a role: that
|
|
180
|
+
envelope starts a family of its own at hop 0.
|
|
181
|
+
|
|
166
182
|
## 4. Outcome rules
|
|
167
183
|
|
|
168
|
-
The plugin sends one completion per task,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
`onlyne_complete` is the only path to `done`. The plugin sends one completion per task,
|
|
185
|
+
at the first of these events:
|
|
186
|
+
|
|
187
|
+
1. **`onlyne_complete`** — the model gives an explicit outcome (`done` by default, or
|
|
188
|
+
`failed` / `cancelled`). A later completion for the same task is refused (not
|
|
189
|
+
re-reported). Its non-empty `text` is the head.
|
|
190
|
+
2. **An errored turn** — the turn ended with a provider error (`stopReason: "error"`).
|
|
191
|
+
That is proof on its own, so the plugin reports `failed` at once, with the error as
|
|
192
|
+
the head.
|
|
193
|
+
3. **The idle ladder** — the turn ended cleanly without a completion, and the task is
|
|
194
|
+
still open. The plugin re-sends the assignment and counts the rung. The idle that
|
|
195
|
+
finds the bound `idleReminders` names already spent reports `failed` — head
|
|
196
|
+
`no completion after <n> idle reminders` — and the session exits the way any
|
|
197
|
+
completion makes it exit.
|
|
198
|
+
4. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
|
|
181
199
|
unsettled task with the host's outcome first, then stops and exits pi.
|
|
182
200
|
|
|
201
|
+
Two things settle nothing: a task that was assigned but whose turn has not run yet (the
|
|
202
|
+
injected message has not executed, so completing now would claim work that never
|
|
203
|
+
happened), and an idle the ladder still has a rung for. The ladder re-sends the
|
|
204
|
+
assignment the task arrived with — the same header, task text and attachment paths the
|
|
205
|
+
injection carried, under one line saying the previous turn ended without a completion —
|
|
206
|
+
and never the role prose, which is already in the session's context. Its images are not
|
|
207
|
+
re-attached: the paths travel as text, so the same bytes are not put into the context
|
|
208
|
+
twice. A new envelope for the same task restarts the count.
|
|
209
|
+
|
|
183
210
|
`head` is a single line, capped at 200 characters; it matches what the client puts in
|
|
184
211
|
`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
|
-
|
|
212
|
+
the explicit `onlyne_complete` call when that call carried one, the error a failed turn
|
|
213
|
+
reported, or the ladder's own line. The last assistant text is the fallback for an
|
|
214
|
+
`onlyne_complete` call that carried no text at all — a sentence spoken after such a call
|
|
215
|
+
cannot replace what the call handed over, and nothing else reads it.
|
|
188
216
|
|
|
189
217
|
A reported completion ends the session's process. `report.complete` goes out as a request,
|
|
190
218
|
and the client answers it only after it has settled the session row, acked the delivery
|
|
@@ -193,7 +221,7 @@ outcome the socket could not carry is queued and flushed after the next `hello`,
|
|
|
193
221
|
flush's answer is the handover that ends the process. A completion the host refused leaves
|
|
194
222
|
the process running, so an exit never loses the task.
|
|
195
223
|
|
|
196
|
-
The last report is one observation with `agent: "idle"
|
|
224
|
+
The last report is one observation with `agent: "idle"`, sent
|
|
197
225
|
after the completion is acknowledged and before the process leaves. The completion settles
|
|
198
226
|
the row from the tuple the client holds, and that tuple still reads `running` when the
|
|
199
227
|
finishing turn was the last heartbeat. Nothing observes the process afterwards, so without
|
|
@@ -215,10 +243,10 @@ workspace, `relay.toml` in a manual installation.
|
|
|
215
243
|
|
|
216
244
|
```toml
|
|
217
245
|
relay_required = ["writer"] # these roles must have received a handoff
|
|
218
|
-
relay_required_count = 2 #
|
|
246
|
+
relay_required_count = 2 # legacy alias of relay_count: this many distinct downstream roles
|
|
219
247
|
```
|
|
220
248
|
|
|
221
|
-
`relay_required` wins when both keys are present.
|
|
249
|
+
`relay_count` is the canonical count key. `relay_required_count` is its legacy alias, the spelling `relay.toml` itself uses. `relay_required` wins when both the list and the count keys are present.
|
|
222
250
|
|
|
223
251
|
The policy belongs in the spec, not in the vendor directory. `onlyne generate --force`
|
|
224
252
|
rewrites the copy this package is vendored into and takes a hand-written `relay.toml`
|
|
@@ -228,8 +256,8 @@ every session process it spawns:
|
|
|
228
256
|
```toml
|
|
229
257
|
[[client]]
|
|
230
258
|
role = "planner"
|
|
259
|
+
relay_count = 2 # this many distinct downstream roles
|
|
231
260
|
relay_required = ["writer"] # these roles must have received a handoff
|
|
232
|
-
relay_count = 2 # ... or this many distinct downstream roles
|
|
233
261
|
```
|
|
234
262
|
|
|
235
263
|
The sources rank `environment > relay.toml > none`: `ONLYNE_RELAY_REQUIRED` (the list,
|
|
@@ -253,7 +281,7 @@ file its turn.
|
|
|
253
281
|
| 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
282
|
| waiver | `force: true` with a non-empty `reason`; it only matters when the guard refuses |
|
|
255
283
|
| 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
|
|
284
|
+
| not guarded | outcomes the plugin reports without the model: an errored turn, the idle ladder's failure, and `recycle{outcome}` |
|
|
257
285
|
|
|
258
286
|
`relay.toml` is a closed subset of TOML: flat `key = value` lines, the two keys above,
|
|
259
287
|
one-line arrays of double-quoted strings, `#` comments. Anything outside that warns on
|
|
@@ -271,18 +299,22 @@ the shipped client.
|
|
|
271
299
|
- **Report sequence base.** The plugin's own `report` sequence starts at 1000, not 1. The
|
|
272
300
|
client stamps its own dispatch events (`created`, resource attach, `ready`) into the
|
|
273
301
|
same `(generation, seq)` watermark, and the reducer silently drops any report at or
|
|
274
|
-
below it (`crates/onlyne-session/src/reconcile
|
|
302
|
+
below it (`crates/onlyne-session/src/reconcile/`). A plugin sequence starting at 1
|
|
275
303
|
would lose its first observations. Everything else about the versioning is per spec.
|
|
276
|
-
- **`observed` is a full `Observation`.** `report.heartbeat` carries the
|
|
304
|
+
- **`observed` is a full `Observation`.** `report.heartbeat` carries the state
|
|
277
305
|
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
|
-
|
|
306
|
+
`mismatch_count`, `agent`, `delivery`, `resource`, `recovery`), not
|
|
307
|
+
a `{"state": "running"}` shorthand: the host deserialises it, overwrites the six
|
|
308
|
+
keys the client owns, and applies only a tuple `is_legal` accepts. This plugin owns the `agent` dimension (turn hooks), the
|
|
309
|
+
`resource` claim — its process is live in the pane the attach was recorded on —
|
|
310
|
+
and the `host` binding. It has no witness for `delivery`, `recovery`,
|
|
311
|
+
`generation_live`, `isolate_after`, `terminate_after` or `mismatch_count`: the
|
|
312
|
+
client rewrites all six from its own intent queue, reducer history and role
|
|
313
|
+
config before the tuple is applied, so whatever this plugin sends there is
|
|
314
|
+
never read. Neither a
|
|
315
|
+
task outcome nor a public view travels in a tuple at all.
|
|
284
316
|
- **`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
|
|
317
|
+
(`crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session`) already reports `ready` when the
|
|
286
318
|
client stages the session for a mounting plugin, so a second report from the plugin is a
|
|
287
319
|
no-op at the host. The plugin sends it anyway: a plugin that mounts *before* any work
|
|
288
320
|
exists is the case the ready barrier names, and it costs one frame.
|
|
@@ -329,7 +361,7 @@ the shipped client.
|
|
|
329
361
|
| `ONLYNE_ROLE` | yes | the mount role |
|
|
330
362
|
| `ONLYNE_SESSION_ID` | yes | mounted session id; `session_id` equals `task_id` in the shipped client |
|
|
331
363
|
| `ONLYNE_TASK_ID` | yes | the task this process serves; drives `session_register` and the initial `ready` |
|
|
332
|
-
| `ONLYNE_SOCKET` | no |
|
|
364
|
+
| `ONLYNE_SOCKET` | no | the socket the client serves for this workspace, injected into every session process it spawns; with the variable unset the plugin reads the marker `<cwd>/.onlyne/run/socket` for the path the daemon published, and falls back to `<cwd>/.onlyne/run/s` |
|
|
333
365
|
| `ONLYNE_RELAY_REQUIRED` | no | the role's spec `relay_required`, comma-joined: the guard's list mode (§5) |
|
|
334
366
|
| `ONLYNE_RELAY_COUNT` | no | the role's spec `relay_count`: the guard's count mode, which decides only when the list is empty (§5) |
|
|
335
367
|
| `ORCA_PANE_KEY` | no | where this process runs (`<tab_id>:<leaf_id>`), reported on every heartbeat as `observed.host.orca.pane_key`; unset outside an Orca pane, which is why the field is then absent |
|
|
@@ -339,9 +371,10 @@ the shipped client.
|
|
|
339
371
|
Constants worth knowing: the plugin heartbeats every 10 s (`heartbeat_timeout_ms` is 30 s),
|
|
340
372
|
allows 5 s for `hello` and 30 s per request, and reconnects on a 1/2/4/8/16/30 s ladder.
|
|
341
373
|
|
|
342
|
-
The plugin reads
|
|
374
|
+
The plugin reads three files of its own: `<cwd>/.pi/onlyne.json` (the switch, §1),
|
|
343
375
|
`relay.toml` next to its `package.json` (the relay policy's fallback, read only when the
|
|
344
|
-
client injected none, §5)
|
|
376
|
+
client injected none, §5), and `<cwd>/.onlyne/run/socket` (the marker naming the socket
|
|
377
|
+
path the client's daemon bound, read when the environment carried none, §8).
|
|
345
378
|
|
|
346
379
|
## 8. Troubleshooting
|
|
347
380
|
|
|
@@ -349,15 +382,16 @@ client injected none, §5).
|
|
|
349
382
|
| --- | --- | --- |
|
|
350
383
|
| `[pi-onlyne] session …` never appears | one of the three env vars is missing, or `enabled` is false | `env \| grep ONLYNE_`; `cat .pi/onlyne.json` |
|
|
351
384
|
| `socket error: connect ENOENT …/.onlyne/run/s` | no `onlyne-client run` for this workspace | start the client, or `onlyne-client status` |
|
|
385
|
+
| `socket error: connect EINVAL …/.onlyne/run/s` on a deep workspace | macOS gives `sun_path` 104 bytes, so a socket path past 103 is refused; a generated role workspace nests three levels under its server root and a long root carries the canonical spelling over the bound. The client serves such a workspace from a short path under the temporary directory and publishes it in `<workspace>/.onlyne/run/socket` | `onlyne-client status` for the line `onlyne: client running … socket <path>`, which names the served path, plus the client log line carrying `socket = <path>`; `cat <workspace>/.onlyne/run/socket` holds that same path, and the plugin dials it when the environment injected nothing |
|
|
352
386
|
| `reconnecting in 4000ms` in a loop | the client is down or the socket was replaced | `onlyne --server-root … roles` |
|
|
353
387
|
| `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 |
|
|
354
388
|
| `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 |
|
|
355
|
-
| ledger stays `in_flight` | no completion
|
|
389
|
+
| 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 |
|
|
356
390
|
| `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 |
|
|
357
391
|
| `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 |
|
|
358
392
|
| `frame_too_large` | a body above 8 MiB | only reachable through an oversize outbound image; the ceiling is the core's |
|
|
359
393
|
| tools missing | `pi.registerTool` is absent in that pi version | `/onlyne status`; the capability table above |
|
|
360
|
-
| session reads `idle` again after `exited` | a heartbeat
|
|
394
|
+
| session reads `idle` again after `exited` | a turn-end heartbeat landed after the completion, moving the agent dimension back | the session log for the report order after `completion`; the plugin stops reporting for a completed task, and the client's own `delivery` survives either way |
|
|
361
395
|
| 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 |
|
|
362
396
|
|
|
363
397
|
`/onlyne status` prints the live state (`connected`, `socket`, `role`, `sessionId`,
|
|
@@ -368,7 +402,7 @@ client injected none, §5).
|
|
|
368
402
|
|
|
369
403
|
```bash
|
|
370
404
|
cd plugins/onlyne-agent-pi
|
|
371
|
-
node --test src/*.test.mjs # framing, protocol, agent state machine, config, relay guard
|
|
405
|
+
node --test src/*.test.mjs # framing, protocol, agent state machine, config, relay guard, socket path
|
|
372
406
|
```
|
|
373
407
|
|
|
374
408
|
`src/agent.live.test.mjs` skips itself unless `target/debug/onlyne-client` and
|
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
|
```
|
|
@@ -22,6 +22,8 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
|
|
|
22
22
|
├─ 任务文本(含图片路径)──► pi user message(deliverAs:"followUp")
|
|
23
23
|
├─ assign_ack{accepted:true}
|
|
24
24
|
├─ report.heartbeat{running|idle} —— 每个 turn,以及任务存续期间每 10 秒
|
|
25
|
+
├─ turn 结束却没有 `onlyne_complete` ──► idle 阶梯:同一条消息再注入
|
|
26
|
+
│ (最多 `idleReminders` 次),之后 `failed` 并退出
|
|
25
27
|
├─ report.complete{outcome, head} —— ledger 的终态事实
|
|
26
28
|
│ └─ client 的应答就是交接点:插件据此让 pi 退出,随后 detach
|
|
27
29
|
├─ probe ──► 一条 heartbeat
|
|
@@ -95,8 +97,9 @@ pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
|
|
|
95
97
|
| --- | --- | --- |
|
|
96
98
|
| `enabled` | `true` | `false` 时该工作区禁用扩展 |
|
|
97
99
|
| `watch.autoStart` | `true` | `false` 时注册工具但不建连接,需 `/onlyne connect` |
|
|
100
|
+
| `idleReminders` | `2` | turn 结束却空闲时重发任务的次数上限(§4);`0` 表示第一次空闲就判失败 |
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
文件缺失即取各自默认值。文件格式错误时打印一行警告,并保留默认值:一个笔误不该静默关掉一个
|
|
100
103
|
role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为 generate 期模板提示),所以
|
|
101
104
|
只有本扩展消费它;键名沿用模板里既有的形状。
|
|
102
105
|
|
|
@@ -143,37 +146,53 @@ png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成
|
|
|
143
146
|
|
|
144
147
|
### `onlyne_complete{outcome?, text?, force?, reason?}`
|
|
145
148
|
|
|
146
|
-
显式结束当前任务,`outcome` 缺省 `done`,也可 `failed
|
|
149
|
+
显式结束当前任务,`outcome` 缺省 `done`,也可 `failed`。它是通向 `done` 的唯一路径:turn 结束时
|
|
150
|
+
没有这次调用,任务会被重发提醒,之后判失败(§4)。`text` 非空时就是 ledger 的 `head`,
|
|
147
151
|
原样写出:空白折叠成单行,截到 200 字符。`text` 缺失或全空白时不带摘要,completion 退回
|
|
148
152
|
最后一段 assistant 文本。这一调用同时结束所在 session 的进程。client 应答完 completion
|
|
149
153
|
报告(见 §4)之后,插件通过 `ctx.shutdown()` 让 pi 退出。pi 0.85.1 没有 tool-result
|
|
150
154
|
`terminate` 处理。工作区带接力策略(§5)时,`force: true` 加非空 `reason` 是绕过一个仍欠着的
|
|
151
155
|
接力的正规通道。
|
|
152
156
|
|
|
157
|
+
### `onlyne_handoff{to, text, image?}`
|
|
158
|
+
|
|
159
|
+
把本会话手上的任务交给家族的下一跳。插件发一个 `handoff` 帧,帧里点名本会话当前持有的任务,
|
|
160
|
+
宿主据此为 `to` 铸一个该家族的子任务:子任务把本任务记为 `parent_task`,hop 加一,家族 id、
|
|
161
|
+
hop 预算、origin、deadline 与 labels 一并随行。工具结果给出子任务 id 与它的 hop。client 拒绝时
|
|
162
|
+
以工具错误原样抛出。`image` 与 send 工具同一含义:png/jpeg/gif/webp 图片的绝对路径。家族带
|
|
163
|
+
hop 预算时,注入的标题行写明 hop 与预算。`onlyne_send{kind: "task"}` 是触达 role 的另一条路:
|
|
164
|
+
那条 envelope 开一个新家族,hop 从 0 起。
|
|
165
|
+
|
|
153
166
|
## 4. outcome 判定规则
|
|
154
167
|
|
|
155
|
-
|
|
168
|
+
`onlyne_complete` 是通向 `done` 的唯一路径。插件每个任务只发一次 completion,取以下四者的先到者:
|
|
156
169
|
|
|
157
|
-
1. **`onlyne_complete`** —— 模型给显式 outcome
|
|
158
|
-
|
|
159
|
-
2.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
1. **`onlyne_complete`** —— 模型给显式 outcome(缺省 `done`,也可 `failed` / `cancelled`)。同一
|
|
171
|
+
任务的第二次 completion 被拒(不重报)。`text` 非空时即 head,原样写出。
|
|
172
|
+
2. **turn 出错** —— turn 以 provider 错误告终(`stopReason: "error"`)。这本身就是证据,插件立即
|
|
173
|
+
报 `failed`,错误信息当 head。
|
|
174
|
+
3. **idle 阶梯** —— turn 干净结束却没有 completion,任务还开着。插件重发任务并记一次;当空闲发现
|
|
175
|
+
`idleReminders` 给的次数已经用完,就报 `failed`(head 为 `no completion after <n> idle
|
|
176
|
+
reminders`),并像任何一次 completion 一样退出 session。
|
|
177
|
+
4. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
|
|
164
178
|
插件并退出 pi。
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
两种情况都不结算:任务已投递但还没跑过任何 turn(注入的消息尚未执行,这时报终态就是撒谎),
|
|
181
|
+
以及阶梯还有余额的那次空闲。阶梯重发的是任务到手时的那条消息——同样的头部、任务文本和附件路径,
|
|
182
|
+
外加一行说明上一轮没有 completion——但不重发 role prose,它已经在上下文里;图片也不重挂,
|
|
183
|
+
路径以文本出现,同样的字节不会进上下文两次。同一任务收到新 envelope 时计数重来。
|
|
184
|
+
|
|
185
|
+
`head` 恒为单行、上限 200 字符,与 client 写入 `out_head` 和回执携带的内容一致。每个任务的 head
|
|
186
|
+
只有一个来源:显式 `onlyne_complete` 带的 `text`(有则原样采用)、出错 turn 报的错误,或阶梯自己
|
|
187
|
+
的那一行。最后一段 assistant 文本只是 `text` 完全缺失的 `onlyne_complete` 的退路——调用之后再说
|
|
188
|
+
的话顶不掉调用交出的内容,此外没有任何东西读它。
|
|
170
189
|
|
|
171
190
|
报出去的 completion 会结束所在 session 的进程。`report.complete` 以请求形式发出,client 只有
|
|
172
191
|
在结算 session 行、ack 掉投递、并写好 `Completion` envelope 之后才应答,插件就在这个应答处
|
|
173
192
|
让 pi 退出。socket 当时送不出去的 outcome 会被记住,并在下一次 `hello` 后补发,那次补发的
|
|
174
193
|
应答就是结束进程的交接点。被宿主拒掉的 completion 不会让进程退出,任务不会因为退出而丢失。
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
最后一条上报只说 agent 这一维:`agent: "idle"` 的观测,发在 completion 被
|
|
177
196
|
ack 之后、进程退出之前。completion 是按 client 手里的元组结算 session 行的,而收尾那一轮
|
|
178
197
|
就是最后一次 heartbeat 时,这个元组读到的仍是 `running`;此后没有任何东西再观测这个进程,
|
|
179
198
|
所以缺了这条上报,已退出的 session 会一直说 `running`。最后一次心跳本来就是 idle 时,插件
|
|
@@ -224,7 +243,7 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
224
243
|
| 作用域 | 本会话自己的投递,仅进程内存:重连不丢,会话重启从空开始,不去猜上一个进程发过什么 |
|
|
225
244
|
| 豁免 | `force: true` 加非空 `reason`;只在守卫拒绝时才起作用 |
|
|
226
245
|
| 审计 | 被豁免的 completion,ledger head 以 `relay-guard-forced: <reason>` 开头;调用带了 `text` 时紧接其后 |
|
|
227
|
-
| 不管的路 |
|
|
246
|
+
| 不管的路 | 插件不经过模型就报出的终态:turn 出错、idle 阶梯用尽,以及 `recycle{outcome}` |
|
|
228
247
|
|
|
229
248
|
`relay.toml` 是 TOML 的封闭子集:扁平的 `key = value` 行、上面两个键、单行双引号字符串数组、
|
|
230
249
|
`#` 注释。子集之外一律 stderr 告警并忽略。它刻意不放 `.onlyne/config.toml`:client 以
|
|
@@ -238,17 +257,19 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
238
257
|
|
|
239
258
|
- **report 序号基址。** 插件自己的 `report` 序号从 1000 起,不是 1。client 把自身的派发事件
|
|
240
259
|
(`created`、资源 attach、`ready`)写进同一个 `(generation, seq)` 水位,reducer 会静默丢弃
|
|
241
|
-
水位及以下的报告(`crates/onlyne-session/src/reconcile
|
|
260
|
+
水位及以下的报告(`crates/onlyne-session/src/reconcile/`),所以从 1 起会丢掉最初的观测。
|
|
242
261
|
其余版本语义与规范一致。
|
|
243
262
|
- **`observed` 是完整的 `Observation`。** `report.heartbeat` 携带整个合法状态元组
|
|
244
263
|
(`version`、`generation_live`、`isolate_after`、`terminate_after`、`mismatch_count`、
|
|
245
|
-
`agent`、`delivery`、`resource`、`recovery
|
|
246
|
-
`{"state": "running"}`
|
|
247
|
-
`agent
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
`agent`、`delivery`、`resource`、`recovery`),不是
|
|
265
|
+
`{"state": "running"}` 这种简写。宿主会反序列化它,先改写 client 主张的那六项,再套用 `is_legal`。本插件主张的
|
|
266
|
+
是 `agent`(turn hooks)、`resource`(进程还活在那块 pane 里,attach 早被宿主派发路径记过)
|
|
267
|
+
与 `host` 绑定这三件事。`delivery`、`recovery`、`generation_live`、`isolate_after`、
|
|
268
|
+
`terminate_after`、`mismatch_count` 它一个见证都没有:client 在归约前会用自己
|
|
269
|
+
的 intent 队列、reducer 历史与角色配置重写这六项,插件填什么都不会被读。任务的 outcome 与公开视图
|
|
270
|
+
更已经不在元组里,所以也就不再上报。
|
|
250
271
|
- **`ready` 每连接报一次。** 宿主的 hand-off 路径
|
|
251
|
-
(`crates/onlyne-client/src/dispatch.rs::hand_session`)在把 session 交给挂载的插件时已经报过
|
|
272
|
+
(`crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session`)在把 session 交给挂载的插件时已经报过
|
|
252
273
|
`ready`,所以插件再报一次在宿主侧是 no-op。插件仍然发送:先挂载、后有活正是 ready barrier
|
|
253
274
|
描述的情形,而且只花一帧。
|
|
254
275
|
- **从不发 `cluster_ref`。** 本插件代表本地 role 说话,从不代表 aggregate;Rust 侧出于同样的
|
|
@@ -284,7 +305,7 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
284
305
|
| `ONLYNE_ROLE` | 是 | 挂载的 role |
|
|
285
306
|
| `ONLYNE_SESSION_ID` | 是 | 挂载的 session id;当前 client 中 session_id 等于 task_id |
|
|
286
307
|
| `ONLYNE_TASK_ID` | 是 | 本进程服务的任务;驱动 `session_register` 与首条 `ready` |
|
|
287
|
-
| `ONLYNE_SOCKET` | 否 |
|
|
308
|
+
| `ONLYNE_SOCKET` | 否 | client 为该工作区实际服务的 socket 路径;凡 client 拉起的会话进程都会带上。变量未设置时,插件读标记文件 `<cwd>/.onlyne/run/socket`,取守护进程发布的那个路径,随后落到 `<cwd>/.onlyne/run/s` |
|
|
288
309
|
| `ONLYNE_RELAY_REQUIRED` | 否 | 该 role 在 spec 里的 `relay_required`,逗号分隔:守卫的名单模式(§5) |
|
|
289
310
|
| `ONLYNE_RELAY_COUNT` | 否 | 该 role 在 spec 里的 `relay_count`:守卫的 count 模式,只在名单为空时起作用(§5) |
|
|
290
311
|
| `ORCA_PANE_KEY` | 否 | 本进程跑在哪(`<tab_id>:<leaf_id>`),每个 heartbeat 以 `observed.host.orca.pane_key` 上报;不在 Orca pane 里时未设置,这也是该字段缺席的原因 |
|
|
@@ -294,8 +315,10 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
294
315
|
值得记住的常量:插件每 10 秒发一次心跳(`heartbeat_timeout_ms` 是 30 秒),`hello` 最多等
|
|
295
316
|
5 秒,单次请求超时 30 秒,重连按 1/2/4/8/16/30 秒阶梯退避。
|
|
296
317
|
|
|
297
|
-
|
|
298
|
-
`relay.toml`(接力策略的兜底,只在 client 没注入策略时才读,§5
|
|
318
|
+
插件自己读三个文件:`<cwd>/.pi/onlyne.json`(开关,§1)、`package.json` 旁边的
|
|
319
|
+
`relay.toml`(接力策略的兜底,只在 client 没注入策略时才读,§5)、
|
|
320
|
+
`<cwd>/.onlyne/run/socket`(标记文件,写明 client 守护进程绑定的 socket 路径,只在环境里
|
|
321
|
+
没带路径时才读,§8)。
|
|
299
322
|
|
|
300
323
|
## 8. 故障排查
|
|
301
324
|
|
|
@@ -303,15 +326,16 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
303
326
|
| --- | --- | --- |
|
|
304
327
|
| 看不到 `[pi-onlyne] session …` | 三个环境变量缺一,或 `enabled` 为 false | `env \| grep ONLYNE_`;`cat .pi/onlyne.json` |
|
|
305
328
|
| `socket error: connect ENOENT …/.onlyne/run/s` | 该工作区没有 `onlyne-client run` | 起 client,或 `onlyne-client status` |
|
|
329
|
+
| 深层工作区里 `socket error: connect EINVAL …/.onlyne/run/s` | macOS 的 `sun_path` 只有 104 字节,超过 103 的 socket 路径会被内核拒绝;生成的 role 工作区在 server root 下再套三层,root 一长,规范写法就越过这个上界。client 面对这种工作区会把 socket 放到临时目录下的短路径上服务,并把选中的路径发布进 `<workspace>/.onlyne/run/socket` | 看 `onlyne-client status` 打印的 `onlyne: client running … socket <路径>`,那一行点出实际服务的路径,再看 client 日志里带 `socket = <路径>` 的那行;`cat <workspace>/.onlyne/run/socket` 得到同一个路径——环境里没带变量时,插件拨的就是它 |
|
|
306
330
|
| 反复 `reconnecting in 4000ms` | client 已停或 socket 被替换 | `onlyne --server-root … roles` |
|
|
307
331
|
| `ready refused: internal: unknown session for …` | 插件为 client 从未暂存的任务报了 ready(手工起 pi 时的正常现象) | 让 client 拉起 pi,而不是手工起 |
|
|
308
332
|
| `assign` 一直不来 | client 的 `session_command` 没能拉起 pi,或 `inject` 被降级 | client 日志里的 spawn 行;`/onlyne status` 看能力集 |
|
|
309
|
-
| ledger 停在 `in_flight` |
|
|
333
|
+
| ledger 停在 `in_flight` | 还没有 completion:没跑过 turn(注入的消息尚未执行),或阶梯还在提醒(`idleReminders`) | pi session 文件里的 `onlyne-assign` 条目和其后注入的提醒;`onlyne` 面板里的 `reminder n of m`;`/onlyne status` 看任务与阶段 |
|
|
310
334
|
| `onlyne_complete` 回答 `relay guard: missing handoff to: …` | 工作区的 spec(或顶替它的 `relay.toml`)点名了一个本会话从未触达的 role | 日常通知显示在 `onlyne` 面板;stderr 保留 `relay guard from …` 等拒绝、socket 错误、超时与帧错误;`required=…` 说明策略;`relay guard: missing handoff …` 列出已投递集合 |
|
|
311
335
|
| `hello` 后立刻 `forbidden` / 断连 | mount role 与 client 的 role 不一致 | `hello.args.mount.role` 对该工作区的 role |
|
|
312
336
|
| `frame_too_large` | 正文超过 8 MiB | 只会由超限的出站图片触发;上限来自核心 |
|
|
313
337
|
| 工具缺失 | 该 pi 版本没有 `pi.registerTool` | `/onlyne status`;对照上面的能力表 |
|
|
314
|
-
| 会话在 `exited` 之后又回到 `idle` | completion
|
|
338
|
+
| 会话在 `exited` 之后又回到 `idle` | completion 之后又落进一条 turn-end heartbeat,把 agent 维搬了回去 | 看 session 日志里 `completion` 之后的 report 顺序;插件对已完成任务不再上报,而 client 自己的 `delivery` 两条都会保住 |
|
|
315
339
|
| supervisor 看板一个 tab 都不列 | 没有 live session 上报过 pane:适配器版本早于这条上报,或这个 pi 不在 Orca pane 里 | `onlyne --server-root … sessions --json` 看 `projection.observed.host.orca.pane_key`;在 pane 里跑 `env \| grep ORCA_` |
|
|
316
340
|
|
|
317
341
|
`/onlyne status` 打印实时状态(`connected`、`socket`、`role`、`sessionId`、`generation`、
|
|
@@ -322,7 +346,7 @@ stderr 告警并忽略,把机会让回文件。
|
|
|
322
346
|
|
|
323
347
|
```bash
|
|
324
348
|
cd plugins/onlyne-agent-pi
|
|
325
|
-
node --test src/*.test.mjs # 帧编解码、协议词汇、agent
|
|
349
|
+
node --test src/*.test.mjs # 帧编解码、协议词汇、agent 状态机、配置、接力守卫、socket 路径
|
|
326
350
|
```
|
|
327
351
|
|
|
328
352
|
`src/agent.live.test.mjs` 只在 `target/debug/onlyne-client` 与 `onlyne-server` 存在时运行。
|