omp-onlyne 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.sample.md +27 -0
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/index.ts +55 -0
- package/package.json +62 -5
- package/src/binding.ts +132 -0
- package/src/bridge-text.ts +109 -0
- package/src/config.ts +300 -0
- package/src/decider-text.ts +420 -0
- package/src/decider.ts +1982 -0
- package/src/drafts.ts +57 -0
- package/src/host-text.ts +86 -0
- package/src/messages.ts +327 -0
- package/src/node.ts +127 -0
- package/src/resources.ts +36 -0
- package/src/runtime.ts +416 -0
- package/src/store.ts +189 -0
- package/src/tern.ts +66 -0
- package/src/todo-native.ts +230 -0
- package/src/todo.ts +896 -0
- package/src/tool-text.ts +133 -0
- package/src/tool.ts +236 -0
- package/src/types.ts +136 -0
- package/src/ui/bootstrap-text.ts +125 -0
- package/src/ui/bootstrap.ts +358 -0
- package/src/ui/command-text.ts +38 -0
- package/src/ui/format.ts +56 -0
- package/src/ui/index.ts +156 -0
- package/src/ui/mail-message.ts +263 -0
- package/src/ui/status.ts +213 -0
- package/src/ui/viewer-text.ts +305 -0
- package/src/ui/viewer.ts +1386 -0
- package/src/urls-text.ts +286 -0
- package/src/urls.ts +1347 -0
package/AGENTS.sample.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Onlyne in this project
|
|
2
|
+
|
|
3
|
+
A starting point for a project that hosts an Onlyne node. Copy what you need into the project's own `AGENTS.md`; the wording assumes the `omp-onlyne` plugin is loaded.
|
|
4
|
+
|
|
5
|
+
## Reaching another node
|
|
6
|
+
|
|
7
|
+
`msg` is the only entry:
|
|
8
|
+
|
|
9
|
+
msg({ delivery_kind: "ask reviewer to check the data", msg: "…", meta: { to: "reviewer" } })
|
|
10
|
+
|
|
11
|
+
`delivery_kind` states the intent in plain language; the judge role turns it into an action. Put the references you already have in `meta` — `to`, `thread`, `in_reply_to {thread,seq}` or `{sender,id}`, `relation`, `message`, `scope`, `seq`.
|
|
12
|
+
|
|
13
|
+
The tool is the whole surface. Do not look for a CLI, do not read the plugin source to learn the protocol, and do not open the node database to find out how it works.
|
|
14
|
+
|
|
15
|
+
## Reading a result
|
|
16
|
+
|
|
17
|
+
The first line of a result is the outcome. `admission: "accepted"` with `guarantee: "durable"` means the receiver recorded the message. `wait: "timed_out"` means no reply arrived within the wait, not that the send failed. Report the outcome and stop.
|
|
18
|
+
|
|
19
|
+
A reply arrives by itself: the plugin injects it as an `onlyne.inbound` message and starts a turn. Do not poll after a send — no inbox scans, thread reads, evidence queries, log reads or database queries. Read state when the user asks about history, or when a fact the result did not carry is genuinely needed.
|
|
20
|
+
|
|
21
|
+
## Subagent sessions
|
|
22
|
+
|
|
23
|
+
A subagent can read its slice but cannot send. Return the reply text and the target so the main session can send it.
|
|
24
|
+
|
|
25
|
+
## Local files
|
|
26
|
+
|
|
27
|
+
`.onlyne/config.toml` holds the node id, region and bind address. The `omp.db` beside it is the node's own record, owned by the plugin; treat it as runtime state, not documentation.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dbydd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,133 @@
|
|
|
1
|
-
#
|
|
1
|
+
# omp-onlyne
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Onlyne v3 for OMP. One model entry, `msg({ delivery_kind, msg, meta })`, backed by
|
|
4
|
+
thread-aware message admission, reply correlation and OMP's `judge` model role.
|
|
5
|
+
|
|
6
|
+
The plugin owns the OMP integration: a local Onlyne HTTP node, project-scoped
|
|
7
|
+
configuration, `onlyne://` reads, custom inbound messages and the `msg` tool.
|
|
8
|
+
The protocol library is [`@onlyne/lib`](../lib).
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
|
|
12
|
+
Configuration belongs to the project at `<project>/.onlyne/config.toml`. It is
|
|
13
|
+
not read from user-level plugin settings, so two projects on one machine keep
|
|
14
|
+
separate identities. `/onlyne init` creates it; no file is required before the
|
|
15
|
+
first run.
|
|
16
|
+
|
|
17
|
+
```toml
|
|
18
|
+
language = "en"
|
|
19
|
+
|
|
20
|
+
[node]
|
|
21
|
+
region = "urn:onlyne:region:research-chamber"
|
|
22
|
+
id = "urn:onlyne:agent:lazuli"
|
|
23
|
+
key_scope = "project"
|
|
24
|
+
|
|
25
|
+
[server]
|
|
26
|
+
bind = "127.0.0.1"
|
|
27
|
+
|
|
28
|
+
[threads]
|
|
29
|
+
page_size = 50
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`region` is the required protocol membership and `id` is the stable node
|
|
33
|
+
identity. They are independent; moving region does not change existing message
|
|
34
|
+
references. `/onlyne init` suggests region from the current Tern session name
|
|
35
|
+
and leaves the node id empty for the user. A short region is saved as
|
|
36
|
+
`urn:onlyne:region:<encoded name>`.
|
|
37
|
+
|
|
38
|
+
The listening port is allocated by the operating system at startup; the runtime
|
|
39
|
+
derives the endpoint and publishes it through discovery. Discovery uses a seed
|
|
40
|
+
address supplied during bootstrap and an HTTP candidate exchange stored in
|
|
41
|
+
`.onlyne/omp.db`. DNS-SD is an optional same-link accelerator: install
|
|
42
|
+
`bonjour-service` to enable it. The configuration does not contain endpoint,
|
|
43
|
+
port, peers, task-spawn policy or supplement windows.
|
|
44
|
+
|
|
45
|
+
`node.key_scope` names where the identity key lives. `project` keeps it in
|
|
46
|
+
`<project>/.onlyne/keys`, which a git work tree must ignore; the node refuses to
|
|
47
|
+
create it otherwise and names both escapes. `global` keeps it in
|
|
48
|
+
`~/.config/onlyne/keys`, shared by every project on the machine. `none` runs the
|
|
49
|
+
keyless loopback development binding, which refuses a non-loopback `bind`. A
|
|
50
|
+
configuration without the key keeps reading the global store.
|
|
51
|
+
|
|
52
|
+
`key_ref` is parsed but not enforced. The current development binding refuses a
|
|
53
|
+
non-empty value and a non-loopback `bind`, because it authenticates with
|
|
54
|
+
`Onlyne-Actor` only. Credential exchange and proof of possession remain a
|
|
55
|
+
separate security proposal; leave both at their defaults for the first E2E.
|
|
56
|
+
|
|
57
|
+
## Surface
|
|
58
|
+
|
|
59
|
+
- `msg` — send, reply, read, manage threads and operate resources. The judge
|
|
60
|
+
role chooses the action and delivery plan; the result reports the plan that
|
|
61
|
+
ran. A new execution context belongs to the receiver; the sender never mints
|
|
62
|
+
one.
|
|
63
|
+
- `onlyne://threads`, `onlyne://threads/<alias>`, `onlyne://threads/<alias>/messages/<seq>`,
|
|
64
|
+
`onlyne://conversations`, `onlyne://conversations/<sender>/<id>`,
|
|
65
|
+
`onlyne://messages/<sender>/<id>`, `onlyne://contexts`, `onlyne://inbox`,
|
|
66
|
+
`onlyne://outbound`, `onlyne://neighbors` — read-only views. List views accept
|
|
67
|
+
`page_size` (or `limit`) and `cursor`; a conversation holds both directions of one
|
|
68
|
+
exchange.
|
|
69
|
+
- `onlyne://resources` reads the local node and peers whose key exchange is complete.
|
|
70
|
+
Each owner returns its authorized resources or an explicit failure. Select
|
|
71
|
+
`onlyne://resources/<encoded-owner>` for one owner's inventory and pagination.
|
|
72
|
+
A resource lives at `onlyne://resources/<encoded-owner>/<encoded-id>`, with
|
|
73
|
+
`/log`, `/log/<event-id>`, `/operations`, and `/operations/<operation-id>` reads.
|
|
74
|
+
Log pages accept an exclusive `since` event id. Resource reads belong to the
|
|
75
|
+
main OMP session because context records contain no resource grants.
|
|
76
|
+
- Inbound messages arrive as `onlyne.inbound` custom messages. A supplement for
|
|
77
|
+
an existing context uses `onlyne.supplement` and `sendCustomMessage`.
|
|
78
|
+
- Tern renders these messages as mailing-list letters: an `[onlyne]` subject,
|
|
79
|
+
sender, date, thread and reply headers, Markdown body, and a link to the
|
|
80
|
+
original entry. Other terminals show the same fields as text. Consumption
|
|
81
|
+
markers stay in the model payload and are hidden from the letter.
|
|
82
|
+
- Consumption is confirmed only after the host observes the entry in an actual
|
|
83
|
+
provider request; submitting a custom message or tool result does not advance
|
|
84
|
+
the record.
|
|
85
|
+
- `/onlyne` opens a domain rail, thread sidebar, message flow and domain node list.
|
|
86
|
+
Threads, messages and nodes scroll independently. The inbox opens from the sidebar.
|
|
87
|
+
Posting names the thread's owner and id; clicking an incoming message selects its
|
|
88
|
+
message reference for a reply. Reply routing currently follows the original
|
|
89
|
+
author and can land in another thread. `/onlyne settings` edits the project
|
|
90
|
+
configuration. `/onlyne init` writes a first config and starts the node; with
|
|
91
|
+
no config, `/onlyne` and `/onlyne settings` open the same bootstrap.
|
|
92
|
+
|
|
93
|
+
Subagent sessions may read only the context slice attached to their own agent
|
|
94
|
+
identity and may not send; the main agent owns the node identity and the reply
|
|
95
|
+
path.
|
|
96
|
+
|
|
97
|
+
The equivalent `msg` query uses `meta: { scope: "resources", op: "list" }`.
|
|
98
|
+
An optional `owner` selects one inventory. The `describe`, `log`, `event`,
|
|
99
|
+
`operations`, and `status` queries name `resource: { owner, id }`.
|
|
100
|
+
`event` requires `event_id`; `status` requires `operation_id`.
|
|
101
|
+
Paged queries accept `page_size` and `cursor`. A resource cursor requires one owner.
|
|
102
|
+
|
|
103
|
+
## Language
|
|
104
|
+
|
|
105
|
+
`/onlyne language en` and `/onlyne language zh-CN` save the project language.
|
|
106
|
+
The command changes rendered text, judge prompts, and the registered `msg`
|
|
107
|
+
description in the current session. Configuration forms preserve this setting.
|
|
108
|
+
The optional top-level `language` setting takes precedence over `ONLYNE_LANG`,
|
|
109
|
+
then `LC_ALL` or `LANG`. The default is English. Route names, protocol fields,
|
|
110
|
+
error codes, diagnostic event names, and user messages keep their original values.
|
|
111
|
+
|
|
112
|
+
`AGENTS.sample.md` is the agent-facing half of that surface. Copy it into the
|
|
113
|
+
host project's `AGENTS.md`: it states that `msg` is the only entry, that a send's
|
|
114
|
+
`admission` is the delivery fact rather than the wait, and that replies arrive as
|
|
115
|
+
inbound messages so a send needs no polling.
|
|
116
|
+
|
|
117
|
+
Select a domain node to request a key exchange. The viewer reads the
|
|
118
|
+
peer's public key and shows both fingerprints before sending. Confirmation sends
|
|
119
|
+
the signed introduction and records local approval. The peer's sidebar and message
|
|
120
|
+
flow show the pending request. Both nodes become trusted after the peer approves. Committed
|
|
121
|
+
trust changes notify the viewer immediately, including introductions received over
|
|
122
|
+
HTTP, so an open viewer does not wait for maintenance to show a request or approval.
|
|
123
|
+
|
|
124
|
+
## Run
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
bun install
|
|
128
|
+
bun run --filter omp-onlyne check
|
|
129
|
+
bun run --filter omp-onlyne test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The package is loaded by OMP through its `omp.extensions` field. It requires
|
|
133
|
+
OMP 18.12.6 or newer.
|
package/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { registerContextBinding } from "./src/binding.ts";
|
|
3
|
+
import { ConfigError } from "./src/config.ts";
|
|
4
|
+
import { registerMessageRenderers, registerProviderBoundary } from "./src/messages.ts";
|
|
5
|
+
import { registerTodoRecovery } from "./src/todo.ts";
|
|
6
|
+
import { startRuntime, stopRuntime, type RuntimeHost } from "./src/runtime.ts";
|
|
7
|
+
import { registerMsgTool } from "./src/tool.ts";
|
|
8
|
+
import { registerOnlyneUrls } from "./src/urls.ts";
|
|
9
|
+
import { attachStatus, registerUi } from "./src/ui/index.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OMP extension entry. The factory runs for every session, including subagents;
|
|
13
|
+
* the network node belongs to the main session only. State recovery and the
|
|
14
|
+
* listener start on `session_start`, never during factory execution.
|
|
15
|
+
*/
|
|
16
|
+
export default function ompOnlyne(pi: ExtensionAPI, host: RuntimeHost = {}): void {
|
|
17
|
+
registerOnlyneUrls();
|
|
18
|
+
registerMessageRenderers(pi);
|
|
19
|
+
registerProviderBoundary(pi);
|
|
20
|
+
registerMsgTool(pi);
|
|
21
|
+
registerContextBinding(pi);
|
|
22
|
+
registerTodoRecovery(pi);
|
|
23
|
+
registerUi(pi);
|
|
24
|
+
let detachStatus: (() => void) | undefined;
|
|
25
|
+
|
|
26
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
27
|
+
if (ctx.agent.kind !== "main") return;
|
|
28
|
+
if (ctx.hasUI) {
|
|
29
|
+
try {
|
|
30
|
+
detachStatus?.();
|
|
31
|
+
detachStatus = attachStatus(ctx);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
pi.logger.warn("onlyne status widget could not be attached", {
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await startRuntime(pi, ctx, host);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// A project without a saved config is the normal first-run state; /onlyne init creates it.
|
|
42
|
+
if (error instanceof ConfigError && error.code === "missing") return;
|
|
43
|
+
pi.logger.warn("onlyne could not start for this project", {
|
|
44
|
+
error: error instanceof Error ? error.message : String(error),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
50
|
+
if (ctx.agent.kind !== "main") return;
|
|
51
|
+
detachStatus?.();
|
|
52
|
+
detachStatus = undefined;
|
|
53
|
+
await stopRuntime();
|
|
54
|
+
});
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "omp-onlyne",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Onlyne v3 for OMP: one msg tool, thread-aware attention and SystemOne delivery decisions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"types": "./index.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"homepage": "https://github.com/dbydd/onlyne#readme",
|
|
10
|
+
"bugs": "https://github.com/dbydd/onlyne/issues",
|
|
11
|
+
"keywords": ["onlyne", "omp", "agent", "messaging", "tools"],
|
|
12
|
+
"publishConfig": { "access": "public" },
|
|
13
|
+
"engines": { "bun": ">=1.4.3" },
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/dbydd/onlyne.git",
|
|
17
|
+
"directory": "packages/omp-onlyne"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.ts",
|
|
21
|
+
"src",
|
|
22
|
+
"AGENTS.sample.md",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"omp": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./index.ts"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./index.ts",
|
|
33
|
+
"./runtime": "./src/runtime.ts",
|
|
34
|
+
"./config": "./src/config.ts",
|
|
35
|
+
"./types": "./src/types.ts"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"check": "tsc --noEmit",
|
|
39
|
+
"test": "bun test"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@onlyne/lib": "^3.0.0",
|
|
43
|
+
"@onlyne/node-host": "^3.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@oh-my-pi/pi-agent-core": "*",
|
|
47
|
+
"@oh-my-pi/pi-coding-agent": "*",
|
|
48
|
+
"@oh-my-pi/pi-tui": "*",
|
|
49
|
+
"omp-todo-more": ">=0.1.4"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"omp-todo-more": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "18.12.6",
|
|
58
|
+
"@oh-my-pi/pi-coding-agent": "18.12.6",
|
|
59
|
+
"@oh-my-pi/pi-tui": "18.12.6",
|
|
60
|
+
"@types/bun": "latest",
|
|
61
|
+
"typescript": "latest"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/binding.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry";
|
|
3
|
+
import { getRuntime, type PluginRuntime } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
let registryRegistered = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Task-spawned sessions are bound to the context alias their dispatch named.
|
|
9
|
+
* The name is only the marker: the registry reference must still prove the
|
|
10
|
+
* session is a child of this node's main agent before any record is bound.
|
|
11
|
+
*/
|
|
12
|
+
export function registerContextBinding(pi: ExtensionAPI): void {
|
|
13
|
+
pi.on("tool_result", async (event, _ctx) => {
|
|
14
|
+
if (event.toolName !== "task") return;
|
|
15
|
+
const runtime = getRuntime();
|
|
16
|
+
if (runtime === undefined) return;
|
|
17
|
+
try {
|
|
18
|
+
const input = event.input;
|
|
19
|
+
const items = Array.isArray(input.tasks) ? input.tasks : [input];
|
|
20
|
+
const details = event.details as
|
|
21
|
+
| { progress?: unknown; results?: unknown }
|
|
22
|
+
| undefined;
|
|
23
|
+
for (const candidate of [...array(details?.progress), ...array(details?.results)]) {
|
|
24
|
+
const id = stringField(candidate, "id");
|
|
25
|
+
if (id === undefined) continue;
|
|
26
|
+
const index = numberField(candidate, "index");
|
|
27
|
+
const name = index === undefined ? undefined : stringField(items[index], "name");
|
|
28
|
+
const alias = aliasOf(name);
|
|
29
|
+
if (alias === undefined) continue;
|
|
30
|
+
await bind(runtime, alias, id);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
pi.logger.warn("onlyne.context.binding.failed", {
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (registryRegistered) return;
|
|
40
|
+
registryRegistered = true;
|
|
41
|
+
AgentRegistry.global().onChange(event => {
|
|
42
|
+
const runtime = getRuntime();
|
|
43
|
+
if (runtime === undefined) return;
|
|
44
|
+
if (event.type === "removed") {
|
|
45
|
+
void updateState(runtime, event.ref.id, "lost").catch(report(pi));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (event.type === "status_changed") {
|
|
49
|
+
void updateState(runtime, event.ref.id, stateOf(event.ref.status)).catch(report(pi));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const alias = aliasOf(event.ref.displayName);
|
|
53
|
+
if (alias === undefined) return;
|
|
54
|
+
void bind(runtime, alias, event.ref.id).catch(report(pi));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function bind(
|
|
59
|
+
runtime: PluginRuntime,
|
|
60
|
+
alias: string,
|
|
61
|
+
agentId: string,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const existing = await runtime.contexts.find(alias);
|
|
64
|
+
if (existing === undefined) return;
|
|
65
|
+
if (existing.node_id !== runtime.config.node.id) return;
|
|
66
|
+
if (existing.main_agent_id !== runtime.mainAgentId) return;
|
|
67
|
+
if (existing.agent_id !== undefined && existing.agent_id !== agentId) return;
|
|
68
|
+
if (existing.agent_id === agentId && existing.session_file !== undefined) return;
|
|
69
|
+
const ref = AgentRegistry.global().get(agentId);
|
|
70
|
+
if (ref === undefined || ref.kind !== "sub" || ref.parentId !== runtime.mainAgentId) return;
|
|
71
|
+
await runtime.contexts.bind(alias, {
|
|
72
|
+
agent_id: agentId,
|
|
73
|
+
...(ref.sessionFile === null || ref.sessionFile === undefined ? {} : { session_file: ref.sessionFile }),
|
|
74
|
+
...(ref.parentId === undefined ? {} : { parent_id: ref.parentId }),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** An alias-recorded update follows the bound agent id, never a display name. */
|
|
79
|
+
async function updateState(
|
|
80
|
+
runtime: PluginRuntime,
|
|
81
|
+
agentId: string,
|
|
82
|
+
state: "idle" | "running" | "parked" | "lost",
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const records = await runtime.contexts.list();
|
|
85
|
+
const record = records.find(candidate =>
|
|
86
|
+
candidate.node_id === runtime.config.node.id &&
|
|
87
|
+
candidate.main_agent_id === runtime.mainAgentId &&
|
|
88
|
+
candidate.agent_id === agentId,
|
|
89
|
+
);
|
|
90
|
+
if (record === undefined) return;
|
|
91
|
+
await runtime.contexts.updateState(record.alias, state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The task name carries the alias; the host may append a numeric suffix to a
|
|
96
|
+
* repeated name. Nothing else about the name is trusted.
|
|
97
|
+
*/
|
|
98
|
+
function aliasOf(name: string | undefined): string | undefined {
|
|
99
|
+
if (name === undefined) return undefined;
|
|
100
|
+
return /^onlyne-(c-[0-9]+)(?:-[0-9]+)?$/.exec(name)?.[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function array(value: unknown): unknown[] {
|
|
104
|
+
return Array.isArray(value) ? value : [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stringField(value: unknown, key: string): string | undefined {
|
|
108
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
109
|
+
const found = (value as Record<string, unknown>)[key];
|
|
110
|
+
return typeof found === "string" && found.length > 0 ? found : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function numberField(value: unknown, key: string): number | undefined {
|
|
114
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
115
|
+
const found = (value as Record<string, unknown>)[key];
|
|
116
|
+
return typeof found === "number" && Number.isInteger(found) ? found : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stateOf(status: string): "idle" | "running" | "parked" | "lost" {
|
|
120
|
+
if (status === "running") return "running";
|
|
121
|
+
if (status === "parked") return "parked";
|
|
122
|
+
if (status === "aborted") return "lost";
|
|
123
|
+
return "idle";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function report(pi: ExtensionAPI): (error: unknown) => void {
|
|
127
|
+
return error => {
|
|
128
|
+
pi.logger.warn("onlyne.context.state.failed", {
|
|
129
|
+
error: error instanceof Error ? error.message : String(error),
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createTranslator } from "@onlyne/node-host/i18n";
|
|
2
|
+
|
|
3
|
+
const en = {
|
|
4
|
+
noDecision: "The inbound entry has no decision.",
|
|
5
|
+
noContextNamed: "The inbound decision named no context.",
|
|
6
|
+
contextLost: "{alias} is no longer bound; it needs an explicit new decision",
|
|
7
|
+
noLiveSession: "The execution context {context} has no live session; it must be bound again before it can receive messages.",
|
|
8
|
+
noTaskTool: "This OMP session has no native task tool.",
|
|
9
|
+
noChildIdentity: "The native task tool returned no child identity.",
|
|
10
|
+
deliveryReferences: "message {message}, thread {thread} seq {seq}",
|
|
11
|
+
subagentPreamble: "Read this onlyne delivery from {author} ({references}).",
|
|
12
|
+
subagentCannotSend: "This session cannot send a message. When you have an answer, call the msg tool once to draft it and report the draft id back:",
|
|
13
|
+
subagentDraftCall: 'msg({ delivery_kind: "draft the reply", msg: <your reply body>, meta: { op: "draft", to: {to}, relation: "reply", in_reply_to: { sender: {sender}, id: {id} } } })',
|
|
14
|
+
subagentNoRetry: "That call stores a draft and sends nothing; the main session sends it. Do not retry a refused send and do not poll for the outcome.",
|
|
15
|
+
subagentSolutionSpace: "The delivery decides the work; use the message as the task.",
|
|
16
|
+
labelInbound: "inbound",
|
|
17
|
+
labelSupplement: "supplement",
|
|
18
|
+
|
|
19
|
+
draftNeedsRelation: "A reply draft needs relation reply or followup and in_reply_to.",
|
|
20
|
+
draftChildScope: "A child may draft a reply only to the source message assigned to its context.",
|
|
21
|
+
draftChildCannotSend: "Return draft_id to the main session; a child cannot send a draft.",
|
|
22
|
+
draftNotFound: "No reply draft is named {id}.",
|
|
23
|
+
draftNeedsReference: "A reply draft needs in_reply_to {sender,id} or {thread,seq}.",
|
|
24
|
+
draftSourceUnavailable: "The draft source message is unavailable.",
|
|
25
|
+
|
|
26
|
+
contextNotRegistered: "No execution context is registered as {alias}.",
|
|
27
|
+
mainSummary: "Main OMP session",
|
|
28
|
+
storeClosed: "The Onlyne store is closed.",
|
|
29
|
+
|
|
30
|
+
todoContextRequired: "A todo decision needs a non-empty context alias.",
|
|
31
|
+
todoUnknownAction: "The judge returned an unknown todo action: {action}.",
|
|
32
|
+
todoAdapterApi: "omp-todo-more is installed but does not expose the todo state adapter API this plugin drives.",
|
|
33
|
+
todoRecordTypes: "omp-todo-more uses branch record types this omp-onlyne build does not understand.",
|
|
34
|
+
todoEmptyAssociate: "The judge chose associate, but the todo list is empty.",
|
|
35
|
+
todoNoSuchItem: 'The judge chose associate, but no todo item is titled "{title}".',
|
|
36
|
+
todoAmbiguousTitle: 'More than one todo item is titled "{title}"; the decision needs a phase.',
|
|
37
|
+
todoDuplicateTitle: 'The judge chose append, but the todo list already has an item titled "{title}"; associate instead.',
|
|
38
|
+
todoPhaseRequired: "An append decision needs a non-empty phase name.",
|
|
39
|
+
todoUnexpectedAction: "Unexpected todo action: {action}",
|
|
40
|
+
todoExplanationRequired: "An append decision needs an explanation or a non-empty message body.",
|
|
41
|
+
todoEstimateInvalid: "A todo estimate must be a positive whole number of minutes.",
|
|
42
|
+
todoMissingField: "The judge chose {action} without a {field}.",
|
|
43
|
+
todoGoalRequired: "The judge chose append into an empty list without the goal's objective, measure and doneWhen.",
|
|
44
|
+
todoNoSession: "No live OMP session owns this inbound todo decision; the todo remains owed by the inbound queue.",
|
|
45
|
+
todoMultipleSessions: "More than one live main OMP session is registered; the todo target is ambiguous.",
|
|
46
|
+
todoSourceLine: "Source: {source}/{seq} from {author}",
|
|
47
|
+
todoDefaultPhase: "Tasks",
|
|
48
|
+
todoBadMetadataVersion: "Onlyne todo decision entry {entry} uses an unsupported metadata version.",
|
|
49
|
+
todoNoDecisionRecord: "Onlyne todo decision entry {entry} records no decision.",
|
|
50
|
+
todoNoOrigins: "Onlyne todo metadata on entry {entry} has no origins array; the association cannot be replayed.",
|
|
51
|
+
todoNoLinks: "Onlyne todo metadata on entry {entry} has no links array; the association cannot be replayed.",
|
|
52
|
+
todoUnknownRecordedAction: "Onlyne todo decision on entry {entry} has an unknown action.",
|
|
53
|
+
todoMissingString: "Onlyne todo metadata on entry {entry} has no string {field}.",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const zh: Record<keyof typeof en, string> = {
|
|
57
|
+
noDecision: "这条入站记录没有对应的决策。",
|
|
58
|
+
noContextNamed: "入站决策没有指定上下文。",
|
|
59
|
+
contextLost: "{alias} 已不再绑定;需要一次显式的新决策",
|
|
60
|
+
noLiveSession: "执行上下文 {context} 没有活动会话;必须先重新绑定才能接收消息。",
|
|
61
|
+
noTaskTool: "这个 OMP 会话没有原生 task 工具。",
|
|
62
|
+
noChildIdentity: "原生 task 工具没有返回子代理标识。",
|
|
63
|
+
deliveryReferences: "消息 {message},线程 {thread} 序号 {seq}",
|
|
64
|
+
subagentPreamble: "阅读这条来自 {author} 的 onlyne 投递({references})。",
|
|
65
|
+
subagentCannotSend: "本会话不能发送消息。得出答案后,调用一次 msg 工具起草回复,并把 draft id 报告回来:",
|
|
66
|
+
subagentDraftCall: 'msg({ delivery_kind: "起草回复", msg: <你的回复正文>, meta: { op: "draft", to: {to}, relation: "reply", in_reply_to: { sender: {sender}, id: {id} } } })',
|
|
67
|
+
subagentNoRetry: "这次调用只保存草稿,不发送任何消息;由主会话负责发送。不要重试被拒绝的发送,也不要轮询结果。",
|
|
68
|
+
subagentSolutionSpace: "投递内容决定要做什么;把消息本身当作任务。",
|
|
69
|
+
labelInbound: "入站",
|
|
70
|
+
labelSupplement: "补充",
|
|
71
|
+
|
|
72
|
+
draftNeedsRelation: "回复草稿需要 relation 为 reply 或 followup,并提供 in_reply_to。",
|
|
73
|
+
draftChildScope: "子代理只能对分配给它的上下文所对应的源消息起草回复。",
|
|
74
|
+
draftChildCannotSend: "把 draft_id 交回主会话;子代理不能发送草稿。",
|
|
75
|
+
draftNotFound: "没有名为 {id} 的回复草稿。",
|
|
76
|
+
draftNeedsReference: "回复草稿需要 in_reply_to {sender,id} 或 {thread,seq}。",
|
|
77
|
+
draftSourceUnavailable: "草稿的源消息不可用。",
|
|
78
|
+
|
|
79
|
+
contextNotRegistered: "没有注册名为 {alias} 的执行上下文。",
|
|
80
|
+
mainSummary: "OMP 主会话",
|
|
81
|
+
storeClosed: "Onlyne 存储已关闭。",
|
|
82
|
+
|
|
83
|
+
todoContextRequired: "todo 决策需要非空的上下文别名。",
|
|
84
|
+
todoUnknownAction: "judge 返回了未知的 todo 动作:{action}。",
|
|
85
|
+
todoAdapterApi: "已安装 omp-todo-more,但它没有暴露本插件所使用的 todo 状态适配接口。",
|
|
86
|
+
todoRecordTypes: "omp-todo-more 使用的分支记录类型是这个 omp-onlyne 构建无法理解的。",
|
|
87
|
+
todoEmptyAssociate: "judge 选择了 associate,但 todo 列表是空的。",
|
|
88
|
+
todoNoSuchItem: "judge 选择了 associate,但没有标题为“{title}”的 todo 项。",
|
|
89
|
+
todoAmbiguousTitle: "有多个 todo 项标题为“{title}”;决策需要指定阶段。",
|
|
90
|
+
todoDuplicateTitle: "judge 选择了 append,但 todo 列表已有标题为“{title}”的项;应改用 associate。",
|
|
91
|
+
todoPhaseRequired: "append 决策需要非空的阶段名。",
|
|
92
|
+
todoUnexpectedAction: "意外的 todo 动作:{action}",
|
|
93
|
+
todoExplanationRequired: "append 决策需要说明内容或非空的消息正文。",
|
|
94
|
+
todoEstimateInvalid: "todo 预估必须是正整数分钟。",
|
|
95
|
+
todoMissingField: "judge 选择了 {action},但没有提供 {field}。",
|
|
96
|
+
todoGoalRequired: "judge 选择了向空列表 append,但没有提供目标的 objective、measure 和 doneWhen。",
|
|
97
|
+
todoNoSession: "没有活动的 OMP 会话持有这条入站 todo 决策;该 todo 仍由入站队列保留。",
|
|
98
|
+
todoMultipleSessions: "注册了多个活动的主 OMP 会话;todo 目标不明确。",
|
|
99
|
+
todoSourceLine: "来源:{source}/{seq},来自 {author}",
|
|
100
|
+
todoDefaultPhase: "任务",
|
|
101
|
+
todoBadMetadataVersion: "条目 {entry} 上的 onlyne todo 决策使用了不支持的元数据版本。",
|
|
102
|
+
todoNoDecisionRecord: "条目 {entry} 上的 onlyne todo 决策没有记录任何决策内容。",
|
|
103
|
+
todoNoOrigins: "条目 {entry} 上的 onlyne todo 元数据没有 origins 数组;无法重放该关联。",
|
|
104
|
+
todoNoLinks: "条目 {entry} 上的 onlyne todo 元数据没有 links 数组;无法重放该关联。",
|
|
105
|
+
todoUnknownRecordedAction: "条目 {entry} 上的 onlyne todo 决策含有未知动作。",
|
|
106
|
+
todoMissingString: "条目 {entry} 上的 onlyne todo 元数据缺少字符串字段 {field}。",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const bridgeText = createTranslator({ en, "zh-CN": zh });
|