@rine-network/eve 0.3.0 → 0.5.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 +12 -6
- package/dist/{channel-WlN3x1wP.js → channel-B81UiHyr.js} +12 -9
- package/dist/channel-core.d.ts +9 -6
- package/dist/channel.d.ts +1 -1
- package/dist/channel.js +1 -1
- package/dist/{client-DsG2xtKs.js → client-CXJATA-m.js} +22 -2
- package/dist/client.d.ts +18 -0
- package/dist/format-groups-list.d.ts +140 -0
- package/dist/format-groups.d.ts +81 -0
- package/dist/format.d.ts +51 -11
- package/dist/index.js +7 -7
- package/dist/onboard.js +2 -2
- package/dist/{registry-Bn4EqPcp.js → registry-DsU13KY3.js} +47 -2
- package/dist/relay.js +2 -2
- package/dist/scaffold-DQ2CA1kD.js +239 -0
- package/dist/scaffold.js +1 -1
- package/dist/schemas-groups-list.d.ts +29 -0
- package/dist/schemas-groups.d.ts +83 -9
- package/dist/schemas.d.ts +26 -6
- package/dist/skill-content.d.ts +8 -2
- package/dist/tool-DeOeMNlK.js +618 -0
- package/dist/tool.d.ts +2 -2
- package/dist/tools/discovery.d.ts +8 -2
- package/dist/tools/groups-admin.d.ts +25 -0
- package/dist/tools/groups-admission.d.ts +56 -0
- package/dist/tools/groups-list.d.ts +21 -0
- package/dist/tools/groups-resolve.d.ts +80 -0
- package/dist/tools/groups.d.ts +18 -14
- package/dist/tools/index.d.ts +5 -3
- package/dist/tools/index.js +3 -3
- package/dist/tools/messaging.d.ts +13 -11
- package/dist/tools-CQDZG-qN.js +1149 -0
- package/dist/types.d.ts +2 -1
- package/dist/webhook.d.ts +8 -1
- package/dist/webhook.js +33 -13
- package/package.json +3 -3
- package/dist/scaffold-3cEUy3jD.js +0 -147
- package/dist/tool-xlLdY8kn.js +0 -276
- package/dist/tools-BhkhV3Mv.js +0 -595
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { n as getRineClient, t as actingAgent } from "./client-CXJATA-m.js";
|
|
2
|
+
import { APIConnectionError, AuthenticationError, AuthorizationError, ConfigError, CryptoError, GROUP_ROSTER_LEAD, NO_GROUPS_IN_ORG, NotFoundError, RateLimitError, RineApiError, RineTimeoutError, SchemaValidationError, ValidationError, groupIsMls, groupMlsInitInFlight, groupMlsNeverFounded, senderKeyExplainer, senderKeyExplanation, unfoundedGroupNote } from "@rine-network/sdk";
|
|
3
|
+
import { resolveApiUrl } from "@rine-network/core";
|
|
4
|
+
import { always, never, once } from "eve/tools/approval";
|
|
5
|
+
import { ZodError } from "zod";
|
|
6
|
+
/**
|
|
7
|
+
* What a group renders in place of an agent list when this org holds no seat.
|
|
8
|
+
*
|
|
9
|
+
* The group list is ORG-scoped — it reports every group any agent of this org
|
|
10
|
+
* is in — so a row on it was never proof that the agent this host is acting as
|
|
11
|
+
* may post there. The empty list is that proof's absence stated plainly: no
|
|
12
|
+
* agent of this org is seated, and a send into the group would be refused.
|
|
13
|
+
*
|
|
14
|
+
* Never omitted, for the reason `NO_CONVERSATION` gives above: a clause a model
|
|
15
|
+
* sees on some rows and not others reads as a rendering fault rather than as a
|
|
16
|
+
* fact about the group. Restated per package because there is no runtime module
|
|
17
|
+
* all seven rendering surfaces import — though this package and its twin do
|
|
18
|
+
* share one (`@rine-network/sdk`), so the day either reworded its row would be
|
|
19
|
+
* the day the sentence belongs there instead.
|
|
20
|
+
*
|
|
21
|
+
* Pinned meanwhile: `tests/test_client-server-contract_group0.py` compares the
|
|
22
|
+
* four declared copies against `rine.group_words.NO_OWN_AGENTS`, which the
|
|
23
|
+
* three Python surfaces import, so a reword in one package fails there rather
|
|
24
|
+
* than shipping two answers to one question.
|
|
25
|
+
*/
|
|
26
|
+
const NO_OWN_AGENTS = "(none — no agent of your org is seated in this group)";
|
|
27
|
+
/**
|
|
28
|
+
* The seated-agents clause, from the one place both renderers read it.
|
|
29
|
+
*
|
|
30
|
+
* `format.ts`'s `renderGroup` calls this too, so the row and the single-group
|
|
31
|
+
* view cannot tell a model two different things about the same group — the same
|
|
32
|
+
* reason `NO_CONVERSATION` is imported there rather than retyped.
|
|
33
|
+
*
|
|
34
|
+
* Each id renders as its HANDLE. The three cases, and why a name is never one
|
|
35
|
+
* of them, are stated once on `AgentHandleMap` in the SDK
|
|
36
|
+
* (`rine-ts-sdk/src/resources/agent-handles.ts`); this renderer is one of the
|
|
37
|
+
* four that follow them. In short: a known id prints its handle, an unknown one
|
|
38
|
+
* prints itself, an empty list prints {@link NO_OWN_AGENTS}. A model handed a
|
|
39
|
+
* UUID cannot tell which of its agents it names — that is the whole reason
|
|
40
|
+
* handle resolution exists here — but a UUID is still addressable, so an
|
|
41
|
+
* unresolved id is degraded, not lost.
|
|
42
|
+
*
|
|
43
|
+
* `handles` is omitted whenever the org's agent list could not be read; that is
|
|
44
|
+
* case 2 for every id, and it is why nothing here throws.
|
|
45
|
+
*
|
|
46
|
+
* `undefined` is accepted on purpose. `GroupReadSchema` defaults the field to
|
|
47
|
+
* `[]`, so a parsed row always has one — but a row that never went through that
|
|
48
|
+
* parse (a hand-built object, a fake) has nothing, and a renderer whose job is
|
|
49
|
+
* to state an absence must not throw on one.
|
|
50
|
+
*/
|
|
51
|
+
function renderOwnAgents(ids, handles) {
|
|
52
|
+
if (!ids || ids.length === 0) return NO_OWN_AGENTS;
|
|
53
|
+
return ids.map((id) => handles?.get(id.toLowerCase()) ?? id).join(", ");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* How this package names a group's regime in one field, for a LIST ROW.
|
|
57
|
+
*
|
|
58
|
+
* One definition for the row, and for the row only. It is NOT what the create
|
|
59
|
+
* confirmation head asks: what a freshly created group runs is decided from the
|
|
60
|
+
* create RESULT, by `createdGroupMls` in `@rine-network/core`, because the row
|
|
61
|
+
* alone cannot tell a founded group from an unfounded one. `renderGroup`'s
|
|
62
|
+
* `[OK]` line re-derives its first clause from the predicates directly and has
|
|
63
|
+
* never called this either. So a reword here reaches this row and nothing else,
|
|
64
|
+
* and a reword in either of those two does not reach this row.
|
|
65
|
+
*
|
|
66
|
+
* The init window is reported as MLS because sends into it already are MLS, and
|
|
67
|
+
* a field that said "sender-key" there would state the opposite of what is
|
|
68
|
+
* happening. The window's own distinction is `rine_group_inspect`'s to make.
|
|
69
|
+
*
|
|
70
|
+
* A group created to run MLS whose ratchet tree was never founded really does
|
|
71
|
+
* carry sender-key traffic, so it names sender keys — and says the rest of the
|
|
72
|
+
* answer, because that row is otherwise field-for-field an open group's and
|
|
73
|
+
* nothing else on the page can tell the two apart. The parenthesis carries NO
|
|
74
|
+
* comma: the row below is comma-separated, which is the same reason its last
|
|
75
|
+
* clause opens with `; `. The whole sentence lives in `rine_group_inspect`; a
|
|
76
|
+
* list row is not the place for it.
|
|
77
|
+
*/
|
|
78
|
+
function groupModeLabel(g) {
|
|
79
|
+
if (groupIsMls(g) || groupMlsInitInFlight(g)) return "MLS";
|
|
80
|
+
if (groupMlsNeverFounded(g)) return "sender-key (MLS never founded)";
|
|
81
|
+
return "sender-key";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One row of the caller's own groups.
|
|
85
|
+
*
|
|
86
|
+
* The row carries `conversation_id` because this is the only place a model can
|
|
87
|
+
* learn it without having already sent into the group or been handed a raw
|
|
88
|
+
* message row: list the groups, take the id off the row, hand it to
|
|
89
|
+
* `rine_thread`. The key is spelled as the wire field rather than as prose so
|
|
90
|
+
* that the string the model reads and the argument it must fill match.
|
|
91
|
+
*
|
|
92
|
+
* It also names which of this org's agents hold a seat here. `member_count` is
|
|
93
|
+
* the whole roster across every org; this clause is the caller's own share of
|
|
94
|
+
* it, and it is the only thing on an org-scoped row that says whether this org
|
|
95
|
+
* can act in the group at all.
|
|
96
|
+
*
|
|
97
|
+
* That clause closes the row with `; ` rather than `, ` because its VALUE is a
|
|
98
|
+
* comma-joined list. Ending a comma-separated row with a comma-containing field
|
|
99
|
+
* makes the row's field count depend on how many seats the org holds, so a
|
|
100
|
+
* reader that split on `, ` would mis-read multi-seat rows only — and read
|
|
101
|
+
* single-seat ones correctly, which is how that would ship.
|
|
102
|
+
*/
|
|
103
|
+
function renderGroupRow(g, handles) {
|
|
104
|
+
const mode = groupModeLabel(g);
|
|
105
|
+
const conversation = g.conversation_id ?? "(none — nothing has been said in this group yet)";
|
|
106
|
+
const yours = renderOwnAgents(g.member_agent_ids, handles);
|
|
107
|
+
return `${g.handle} (id ${g.id}) — ${mode}, enrollment ${g.enrollment_policy}, ${g.member_count} member(s), conversation_id=${conversation}; your agents: ${yours}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A numbered list of the groups this org's agents are seated in, or the
|
|
111
|
+
* empty-state line.
|
|
112
|
+
*
|
|
113
|
+
* The list is org-scoped, and each row's `your agents` clause names which of
|
|
114
|
+
* this org's agents are seated in that group — an empty clause means none of
|
|
115
|
+
* them is, and a send into that group would be refused. The clause names them
|
|
116
|
+
* by handle because that is the spelling `rine_whoami` prints, so the two
|
|
117
|
+
* outputs can be read against each other — a UUID matched nothing this surface
|
|
118
|
+
* had ever shown the model. What neither tool says is which agent this host is
|
|
119
|
+
* acting AS: `WhoAmI` carries no such field and `renderWhoAmI` marks no row, so
|
|
120
|
+
* in a multi-agent org that still has to come from the host's own config (the
|
|
121
|
+
* `agent` this client was built with, which may itself have been given as a
|
|
122
|
+
* UUID).
|
|
123
|
+
*/
|
|
124
|
+
function renderGroups(items, handles) {
|
|
125
|
+
if (items.length === 0) return "This agent belongs to no groups.";
|
|
126
|
+
return items.map((g, i) => `${i + 1}. ${renderGroupRow(g, handles)}`).join("\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One directory row.
|
|
130
|
+
*
|
|
131
|
+
* No encryption mode: a directory result carries no MLS latch at all, so
|
|
132
|
+
* naming one here would be an invention. `rine_group_inspect` answers that
|
|
133
|
+
* question, and only for a group an agent of this org is already in.
|
|
134
|
+
*/
|
|
135
|
+
function renderGroupSummaryRow(g) {
|
|
136
|
+
const desc = g.description ? ` — ${g.description}` : "";
|
|
137
|
+
return `${g.handle} (id ${g.id}) — enrollment ${g.enrollment_policy}, ${g.member_count} member(s)${desc}`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A numbered list of publicly listed groups, or the empty-state line.
|
|
141
|
+
*
|
|
142
|
+
* The header says what the list IS. `GET /directory/groups` is an
|
|
143
|
+
* unauthenticated public-visibility scan across every org and carries no
|
|
144
|
+
* identity at all, so nothing on a row can say whether this agent holds a seat
|
|
145
|
+
* in that group — while `renderGroups` above prints near-identical rows that
|
|
146
|
+
* end with exactly that. Two numbered lists told apart by two words in a header
|
|
147
|
+
* is not enough, so this one says the difference outright.
|
|
148
|
+
*
|
|
149
|
+
* It names no verb: the header states what the caller is looking at, and which
|
|
150
|
+
* tool to reach for next is the tool description's sentence, not this one's.
|
|
151
|
+
*/
|
|
152
|
+
function renderGroupSummaries(items) {
|
|
153
|
+
if (items.length === 0) return "No public groups matched.";
|
|
154
|
+
return [`${items.length} public group(s) found in the directory. A row here is a group that exists, not a group this agent is in:`, ...items.map((g, i) => `${i + 1}. ${renderGroupSummaryRow(g)}`)].join("\n");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One roster row: who they are, what they may do, and since when.
|
|
158
|
+
*
|
|
159
|
+
* The join date renders date-only, and the same way on every surface that
|
|
160
|
+
* renders a roster: `joined_at` is a validated ISO-8601 timestamp on the wire
|
|
161
|
+
* (`GroupMemberSchema`), and a model reading seven surfaces should not have to
|
|
162
|
+
* parse three renderings of the same field.
|
|
163
|
+
*
|
|
164
|
+
* A row of this org's own is suffixed `(yours)`. That is a MARKER, not a
|
|
165
|
+
* filter: every member of the group is listed whichever org holds the seat, and
|
|
166
|
+
* the suffix only says which of them this host could act as.
|
|
167
|
+
*/
|
|
168
|
+
function renderMemberRow(m) {
|
|
169
|
+
const joined = m.joined_at.slice(0, 10);
|
|
170
|
+
const own = m.is_own_org ? " (yours)" : "";
|
|
171
|
+
return `${m.agent_handle ?? m.agent_id} — ${m.role}, joined ${joined}${own}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A numbered roster, or the empty-state line.
|
|
175
|
+
*
|
|
176
|
+
* The roster is members only. An invitation and a nomination hold a seat
|
|
177
|
+
* against the group's ceiling without appearing here — `rine_group_requests`
|
|
178
|
+
* is what reports those — so a roster shorter than the ceiling suggests is not
|
|
179
|
+
* a gap in this list.
|
|
180
|
+
*
|
|
181
|
+
* Every member is listed, from every org. Nothing here filters on whose agent
|
|
182
|
+
* a row is; `(yours)` marks this org's rows and takes none away.
|
|
183
|
+
*/
|
|
184
|
+
function renderRoster(items) {
|
|
185
|
+
if (items.length === 0) return "No members reported for this group.";
|
|
186
|
+
return items.map((m, i) => `${i + 1}. ${renderMemberRow(m)}`).join("\n");
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/format.ts
|
|
190
|
+
/**
|
|
191
|
+
* Pure render functions: turn SDK return values into the human-readable strings
|
|
192
|
+
* tools + the channel hand to the LLM / send back over rine.
|
|
193
|
+
*
|
|
194
|
+
* These read ONLY `plaintext` / `decrypt_error` / verification
|
|
195
|
+
* fields. They NEVER read `encrypted_payload` or any envelope/ciphertext field,
|
|
196
|
+
* so ciphertext can never reach the LLM context through a rendered string.
|
|
197
|
+
*/
|
|
198
|
+
/** One thread turn as a role-tagged line: `[sent] you: …` / `[received] alice@org: …`. */
|
|
199
|
+
function renderThreadLine(e) {
|
|
200
|
+
const who = e.direction === "sent" ? "you" : e.senderHandle ?? "unknown";
|
|
201
|
+
return `[${e.direction}] ${who}: ${e.text}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Render a both-sided transcript (oldest→newest) for `rine_thread`. Each turn is
|
|
205
|
+
* a role-tagged line; `[unavailable]` text passes through unchanged.
|
|
206
|
+
*/
|
|
207
|
+
function renderThread(entries) {
|
|
208
|
+
if (entries.length === 0) return "No messages in this conversation.";
|
|
209
|
+
return entries.map(renderThreadLine).join("\n");
|
|
210
|
+
}
|
|
211
|
+
/** Honest signature note — never claims "verified" for an unverifiable message. */
|
|
212
|
+
function verifiedNote(msg) {
|
|
213
|
+
return msg.verified ? "signature verified" : `signature ${msg.verification_status}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* THE PLAINTEXT-IS-JSON FOOTGUN. Outbound sends wrap `{ text: body }`, and the
|
|
217
|
+
* SDK auto-`JSON.parse`s inbound `application/json` plaintext into a structured
|
|
218
|
+
* value. Unwrap defensively so the model sees prose, never raw JSON:
|
|
219
|
+
* - a string → returned as-is
|
|
220
|
+
* - `{ text: "…" }` → the inner text
|
|
221
|
+
* - anything else → compact JSON (last resort)
|
|
222
|
+
*/
|
|
223
|
+
function unwrapText(plaintext) {
|
|
224
|
+
if (typeof plaintext === "string") return plaintext;
|
|
225
|
+
if (plaintext === null || plaintext === void 0) return "";
|
|
226
|
+
if (typeof plaintext === "object") {
|
|
227
|
+
const text = plaintext.text;
|
|
228
|
+
if (typeof text === "string") return text;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return JSON.stringify(plaintext);
|
|
232
|
+
} catch {
|
|
233
|
+
return String(plaintext);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Body of a message: decrypt error if unreadable, else the unwrapped plaintext.
|
|
238
|
+
*
|
|
239
|
+
* `explain` is the per-render tiering: pass it on a MULTI render (an inbox,
|
|
240
|
+
* one call per `renderInbox()`) so the no-sender-key cause explains itself in
|
|
241
|
+
* full on the first unreadable row and marks every subsequent one. Omit it on
|
|
242
|
+
* a SINGLE render (`rine_read`, a `rine_send_and_wait` reply, the webhook
|
|
243
|
+
* channel's own inbound render) so the one row keeps the full sentence it
|
|
244
|
+
* already carries — there is nothing to tier when the caller asked for
|
|
245
|
+
* exactly one message. Every other decrypt-error cause is untouched either
|
|
246
|
+
* way; classification is the typed `decrypt_error_code`, never the words in
|
|
247
|
+
* `decrypt_error`.
|
|
248
|
+
*/
|
|
249
|
+
function renderMessageBody(msg, explain) {
|
|
250
|
+
if (msg.decrypt_error) {
|
|
251
|
+
const explanation = explain ? senderKeyExplanation(msg) : null;
|
|
252
|
+
return `[unreadable] ${explain && explanation ? explain(explanation).line : msg.decrypt_error}`;
|
|
253
|
+
}
|
|
254
|
+
return unwrapText(msg.plaintext);
|
|
255
|
+
}
|
|
256
|
+
/** Sender label: prefer the human handle, fall back to the agent UUID. */
|
|
257
|
+
function senderLabel(msg) {
|
|
258
|
+
return msg.sender_handle ?? msg.from_agent_id ?? "unknown sender";
|
|
259
|
+
}
|
|
260
|
+
/** A single message rendered across multiple labeled lines (for `rine_read`). */
|
|
261
|
+
function renderSingleMessage(msg) {
|
|
262
|
+
const lines = [
|
|
263
|
+
`Message ${msg.id}`,
|
|
264
|
+
`from: ${senderLabel(msg)}`,
|
|
265
|
+
`type: ${msg.type}`
|
|
266
|
+
];
|
|
267
|
+
if (msg.group_handle ?? msg.group_id) lines.push(`group: ${msg.group_handle ?? msg.group_id}`);
|
|
268
|
+
lines.push(`body: ${renderMessageBody(msg)}`);
|
|
269
|
+
lines.push(`(${verifiedNote(msg)})`);
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
/** One inbox row: compact single line keyed by id + sender + body preview. */
|
|
273
|
+
function renderInboxRow(msg, explain) {
|
|
274
|
+
const from = senderLabel(msg);
|
|
275
|
+
const where = msg.group_handle ? ` in ${msg.group_handle}` : "";
|
|
276
|
+
return `${msg.id} from ${from}${where}: ${renderMessageBody(msg, explain)} (${verifiedNote(msg)})`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* A numbered inbox list, or the empty-state line.
|
|
280
|
+
*
|
|
281
|
+
* MULTI render: one explainer per `renderInbox()` call, so the
|
|
282
|
+
* no-sender-key cause explains itself once for this render and marks every
|
|
283
|
+
* later row with the same cause — never module-level, never per-row.
|
|
284
|
+
*/
|
|
285
|
+
function renderInbox(items) {
|
|
286
|
+
if (items.length === 0) return "No new messages.";
|
|
287
|
+
const explain = senderKeyExplainer();
|
|
288
|
+
return items.map((msg, i) => `${i + 1}. ${renderInboxRow(msg, explain)}`).join("\n");
|
|
289
|
+
}
|
|
290
|
+
/** One discovery row. */
|
|
291
|
+
function renderAgentSummary(a) {
|
|
292
|
+
const verified = a.verified ? " [verified]" : "";
|
|
293
|
+
const desc = a.description ? ` — ${a.description}` : "";
|
|
294
|
+
const cat = a.category ? ` (${a.category})` : "";
|
|
295
|
+
return `${a.handle}${verified}${cat}${desc}`;
|
|
296
|
+
}
|
|
297
|
+
/** A numbered discovery list, or the empty-state line. */
|
|
298
|
+
function renderDiscover(items) {
|
|
299
|
+
if (items.length === 0) return "No agents matched.";
|
|
300
|
+
return items.map((a, i) => `${i + 1}. ${renderAgentSummary(a)}`).join("\n");
|
|
301
|
+
}
|
|
302
|
+
/** A full agent profile (for `rine_inspect`). */
|
|
303
|
+
function renderProfile(p) {
|
|
304
|
+
const lines = [
|
|
305
|
+
`${p.name} (${p.handle})`,
|
|
306
|
+
`id: ${p.id}`,
|
|
307
|
+
`verified: ${p.verified ? "yes" : "no"}`,
|
|
308
|
+
`human oversight: ${p.human_oversight ? "yes" : "no"}`
|
|
309
|
+
];
|
|
310
|
+
if (p.category) lines.push(`category: ${p.category}`);
|
|
311
|
+
if (p.description) lines.push(`description: ${p.description}`);
|
|
312
|
+
return lines.join("\n");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* This agent's own identity, for `rine_whoami`: the org it belongs to, the
|
|
316
|
+
* trust tier that org holds, and every handle it can act as.
|
|
317
|
+
*
|
|
318
|
+
* Revoked agents are dropped. The route returns them, and naming one here
|
|
319
|
+
* would offer the model a handle nothing can be sent from or to.
|
|
320
|
+
*/
|
|
321
|
+
function renderWhoAmI(me) {
|
|
322
|
+
const slug = me.org.slug ? ` (${me.org.slug})` : "";
|
|
323
|
+
const live = me.agents.filter((a) => !a.revoked_at).map((a) => a.handle);
|
|
324
|
+
return [
|
|
325
|
+
`Org ${me.org.name}${slug}`,
|
|
326
|
+
`trust tier: ${me.trust_tier}`,
|
|
327
|
+
`agents: ${live.length > 0 ? live.join(", ") : "none live"}`
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* A group rendered for `rine_group_inspect`. Every branch is `[OK]` — an MLS
|
|
332
|
+
* group is readable/postable from here.
|
|
333
|
+
*
|
|
334
|
+
* Four states, not two. The second is the MLS init window: the group has no
|
|
335
|
+
* `mls_group_id` to render yet, while the server already accepts nothing but
|
|
336
|
+
* MLS there, so sends from here are MLS sends. Rendering that window as either
|
|
337
|
+
* of the other states says the opposite of what is happening.
|
|
338
|
+
*
|
|
339
|
+
* The third is a group created to run MLS whose ratchet tree was never founded.
|
|
340
|
+
* It reads to every other question exactly like the fourth — an ordinary
|
|
341
|
+
* sender-key group — so it used to fall into it, and the agent was never told
|
|
342
|
+
* the group had not got the encryption it was created for. It stays `[OK]`:
|
|
343
|
+
* every claim that marker makes is true here, the group carries traffic, and
|
|
344
|
+
* only the regime is other than intended. The sentence is the SDK's, and this
|
|
345
|
+
* surface asks for the no-verb spelling because `rine_group_reclaim` refuses on
|
|
346
|
+
* eve — naming a verb that will not act is worse than naming none.
|
|
347
|
+
*
|
|
348
|
+
* `your agents` closes the gap the `[OK]` lines leave open: they describe how
|
|
349
|
+
* the GROUP is run, not whether this org may act in it. A group can be perfectly
|
|
350
|
+
* readable in the abstract and hold no seat of this org's at all. It names them
|
|
351
|
+
* by handle when `handles` resolves them — the rule is stated once on
|
|
352
|
+
* `AgentHandleMap` in the SDK, and `renderOwnAgents` is where it is applied.
|
|
353
|
+
*/
|
|
354
|
+
function renderGroup(g, handles) {
|
|
355
|
+
const lines = [`Group ${g.handle} (id ${g.id})`];
|
|
356
|
+
if (groupIsMls(g)) {
|
|
357
|
+
lines.push("[OK] MLS group — end-to-end encrypted (RFC 9420), readable/postable from here.");
|
|
358
|
+
if (g.mls_group_id) lines.push(`mls_group_id: ${g.mls_group_id}`);
|
|
359
|
+
} else if (groupMlsInitInFlight(g)) lines.push("[OK] MLS group, initialising — end-to-end encrypted. Sends from here already use MLS.");
|
|
360
|
+
else if (groupMlsNeverFounded(g)) lines.push(`[OK] sender-key group — readable/postable from here. ${unfoundedGroupNote()}`);
|
|
361
|
+
else lines.push("[OK] sender-key group — readable/postable from here.");
|
|
362
|
+
lines.push(`enrollment: ${g.enrollment_policy}`);
|
|
363
|
+
lines.push(`visibility: ${g.visibility}`);
|
|
364
|
+
lines.push(`conversation_id: ${g.conversation_id ?? "(none — nothing has been said in this group yet)"}`);
|
|
365
|
+
lines.push(`your agents: ${renderOwnAgents(g.member_agent_ids, handles)}`);
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
/** `rine_group_join` outcome: immediate membership vs a pending vote. */
|
|
369
|
+
function renderJoinResult(target, result) {
|
|
370
|
+
if (result.status === "joined") return `Joined ${target}.`;
|
|
371
|
+
return `Join request for ${target} submitted — pending approval (request ${result.request.id}).`;
|
|
372
|
+
}
|
|
373
|
+
/** One pending-invite row: group, inviter, status, and any message. */
|
|
374
|
+
function renderInviteRow(inv) {
|
|
375
|
+
const group = inv.group_handle ?? inv.group_name ?? inv.group_id;
|
|
376
|
+
const from = inv.invited_by ? ` invited by ${inv.invited_by}` : "";
|
|
377
|
+
const msg = inv.message ? `: "${inv.message}"` : "";
|
|
378
|
+
return `${group} (status ${inv.status})${from}${msg}`;
|
|
379
|
+
}
|
|
380
|
+
/** A numbered list of the caller's pending group invites, or the empty-state line. */
|
|
381
|
+
function renderInvites(items) {
|
|
382
|
+
if (items.length === 0) return "No pending group invites.";
|
|
383
|
+
return items.map((inv, i) => `${i + 1}. ${renderInviteRow(inv)}`).join("\n");
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/errors.ts
|
|
387
|
+
/**
|
|
388
|
+
* `formatError(err)` — turns any thrown SDK error into a readable
|
|
389
|
+
* string for the LLM, never a stack trace. Tools wrap their one `await client.*`
|
|
390
|
+
* call in `try/catch → formatError` and RESOLVE (never reject) for mapped errors.
|
|
391
|
+
*
|
|
392
|
+
* Ordering matters: the TS SDK error classes form a hierarchy rooted at
|
|
393
|
+
* `RineApiError`, so this maps MOST-SPECIFIC FIRST and lets `RineApiError` be the
|
|
394
|
+
* catch-all for API errors.
|
|
395
|
+
*/
|
|
396
|
+
/** The synchronous message thrown by `sendAndWait` for a `#` group handle. */
|
|
397
|
+
const GROUP_ON_WAIT_PREFIX = "sendAndWait() does not support group handles";
|
|
398
|
+
/**
|
|
399
|
+
* True when `err` is the plain `Error` `sendAndWait` throws for a group handle.
|
|
400
|
+
* The `send_and_wait` tool checks this BEFORE `formatError` and returns the
|
|
401
|
+
* "1:1 only" guidance; it is not a typed SDK error class.
|
|
402
|
+
*/
|
|
403
|
+
function isGroupUnsupportedOnWait(err) {
|
|
404
|
+
return err instanceof Error && err.message.startsWith(GROUP_ON_WAIT_PREFIX);
|
|
405
|
+
}
|
|
406
|
+
/** The fixed reply for the group-on-`sendAndWait` case. */
|
|
407
|
+
const GROUP_ON_WAIT_MESSAGE = "rine_send_and_wait is 1:1 only; use rine_send for groups.";
|
|
408
|
+
/**
|
|
409
|
+
* A group 404 has THREE producers on this stack, two spellings, and only one of
|
|
410
|
+
* them is a `NotFoundError`.
|
|
411
|
+
*
|
|
412
|
+
* - the **server** answers a bare `Group not found` (404 → `NotFoundError`);
|
|
413
|
+
* - the **TS SDK** resolves the handle itself before every group send and
|
|
414
|
+
* throws a PLAIN `Error`, `Group not found: <handle>`
|
|
415
|
+
* (`rine-ts-sdk/src/resources/messages.ts` `resolveGroup`;
|
|
416
|
+
* `rine-core`'s `getOrCreateSenderKey` throws the same shape);
|
|
417
|
+
* - the SDK's group **resolver** (`groups.resolveRef`) refuses the same way
|
|
418
|
+
* for every group verb on this surface, and its refusal is a plain `Error`
|
|
419
|
+
* carrying the same lead plus the caller's own roster.
|
|
420
|
+
*
|
|
421
|
+
* 🔴 `err instanceof NotFoundError` is therefore not the test. Keyed on the
|
|
422
|
+
* class alone, the commonest group failure there is — a mistyped handle on
|
|
423
|
+
* `rine_send` — fell past the branch to the bare `String(err.message)` arm with
|
|
424
|
+
* no remedy at all. The predicate reads the MESSAGE, whichever class carried
|
|
425
|
+
* it.
|
|
426
|
+
*
|
|
427
|
+
* This package used to mint a third spelling of its own — `No group '<x>' among
|
|
428
|
+
* the groups your org's agents belong to`, thrown by a local `groups.list()`
|
|
429
|
+
* match — and it went when that local match did. Nothing on this stack
|
|
430
|
+
* produces that sentence any more, so nothing here reads for it: a predicate
|
|
431
|
+
* kept for a producer that no longer exists is a claim that the surface can
|
|
432
|
+
* still say something it cannot.
|
|
433
|
+
*
|
|
434
|
+
* A group needs the group half of the directory: `rine_discover` searches
|
|
435
|
+
* AGENTS and can never resolve a group handle, so pointing a missing group at
|
|
436
|
+
* it sends the caller to a search that structurally cannot answer.
|
|
437
|
+
*/
|
|
438
|
+
const GROUP_NOT_FOUND_LEAD = "Group not found";
|
|
439
|
+
/**
|
|
440
|
+
* The SECOND lead, and the one a BARE recipient name now carries.
|
|
441
|
+
*
|
|
442
|
+
* The bare-name miss stopped leading with a group verdict — both namespaces
|
|
443
|
+
* have answered no by then, and the sentence says so — but the REMEDY below is
|
|
444
|
+
* unchanged, and that is the whole reason this constant exists here. Dropping
|
|
445
|
+
* the first lead without adding this one routes every bare-name miss to
|
|
446
|
+
* `formatError`'s catch-all, which names `rine_discover`: the public AGENT
|
|
447
|
+
* directory, which cannot resolve a group handle. That is exactly the
|
|
448
|
+
* misdirection this constant exists to prevent, restored silently, with every
|
|
449
|
+
* suite green.
|
|
450
|
+
*
|
|
451
|
+
* Pinned against `rine-core`, `rine-sdk` and the four other reading surfaces by
|
|
452
|
+
* `NEITHER_NAMESPACE_LEAD_COPIES` (`tests/helpers_client_paths.py`).
|
|
453
|
+
*/
|
|
454
|
+
const NEITHER_NAMESPACE_LEAD = "names neither an agent nor a group you can reach";
|
|
455
|
+
/**
|
|
456
|
+
* True for any group-404 spelling, whatever class carried it.
|
|
457
|
+
*
|
|
458
|
+
* 🔴 The second lead is matched by CONTAINMENT, not prefix: its sentence opens
|
|
459
|
+
* with the caller's own spelling (`'kofi' names neither …`), so `startsWith`
|
|
460
|
+
* cannot see it and the arm would read as "not a group problem" forever.
|
|
461
|
+
*/
|
|
462
|
+
function isGroupNotFound(text) {
|
|
463
|
+
return text.startsWith(GROUP_NOT_FOUND_LEAD) || text.includes(NEITHER_NAMESPACE_LEAD);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The one remedy all three take.
|
|
467
|
+
*
|
|
468
|
+
* `rine_groups` is named FIRST because it resolves a group the caller's org
|
|
469
|
+
* holds a seat in, public or private. `rine_discover_groups` searches the
|
|
470
|
+
* directory, which the server filters to `visibility == "public"`, so it
|
|
471
|
+
* structurally cannot answer for a private group — it is named second because
|
|
472
|
+
* it is the right tool for finding a public group you have never joined.
|
|
473
|
+
*/
|
|
474
|
+
const GROUP_NOT_FOUND_REMEDY = "Try rine_groups to find the right group handle, or rine_discover_groups to search the public directory.";
|
|
475
|
+
/** The half of that remedy that is still worth naming once the roster is in hand. */
|
|
476
|
+
const SEARCH_THE_DIRECTORY = "Try rine_discover_groups to search the public directory.";
|
|
477
|
+
/**
|
|
478
|
+
* A group refusal, plus the tool that can answer it — and one verb fewer when
|
|
479
|
+
* the refusal has already answered itself.
|
|
480
|
+
*
|
|
481
|
+
* The SDK resolves a group reference against the caller's own groups and, on a
|
|
482
|
+
* miss, hands back the list it was holding: {@link GROUP_ROSTER_LEAD} opens the
|
|
483
|
+
* roster, {@link NO_GROUPS_IN_ORG} is the arm for an org that holds a seat
|
|
484
|
+
* nowhere. Printing "run `rine_groups`" underneath either one tells a model to
|
|
485
|
+
* go and fetch what it is already reading — the `rine_discover` defect
|
|
486
|
+
* inverted, and measured to be worse than a wasted call: models parrot the hint
|
|
487
|
+
* back as their answer instead of acting on it. `rine_discover_groups` is then
|
|
488
|
+
* the only verb that can still find something new, so it is the only one named.
|
|
489
|
+
* A **server** group 404 carries no roster and keeps both verbs.
|
|
490
|
+
*
|
|
491
|
+
* The roster arm ends on a LIST ROW, not on a sentence, so the clause joins
|
|
492
|
+
* with a newline and the lead's own full stop is dropped: `(Planning).` reads
|
|
493
|
+
* as part of that last group's entry, and a caller copying the row it was told
|
|
494
|
+
* to name copies the period into the retry.
|
|
495
|
+
*
|
|
496
|
+
* Both sentences are imported from the SDK rather than retyped, so a reword
|
|
497
|
+
* there cannot leave a stale literal here quietly restoring the loop.
|
|
498
|
+
*/
|
|
499
|
+
function groupNotFoundMessage(detail) {
|
|
500
|
+
if (detail.includes(GROUP_ROSTER_LEAD)) return `Not found: ${detail}\n${SEARCH_THE_DIRECTORY}`;
|
|
501
|
+
if (detail.includes(NO_GROUPS_IN_ORG)) return `Not found: ${detail} ${SEARCH_THE_DIRECTORY}`;
|
|
502
|
+
return `Not found: ${detail.replace(/\.+$/, "")}. ${GROUP_NOT_FOUND_REMEDY}`;
|
|
503
|
+
}
|
|
504
|
+
function formatError(err) {
|
|
505
|
+
if (err instanceof ZodError) {
|
|
506
|
+
const first = err.issues[0];
|
|
507
|
+
return `Invalid input: ${first?.path.length ? first.path.join(".") : "input"} — ${first?.message ?? "validation failed"}.`;
|
|
508
|
+
}
|
|
509
|
+
if (err instanceof AuthenticationError) return "rine auth failed — onboard with `npx @rine-network/eve onboard` or set RINE_CONFIG_DIR to your credentials directory.";
|
|
510
|
+
if (err instanceof AuthorizationError) return `Not authorized: ${err.detail}.`;
|
|
511
|
+
if (err instanceof NotFoundError) {
|
|
512
|
+
if (isGroupNotFound(err.detail)) return groupNotFoundMessage(err.detail);
|
|
513
|
+
return `Not found: ${err.detail}. Try rine_discover to find the right handle.`;
|
|
514
|
+
}
|
|
515
|
+
if (err instanceof RateLimitError) return `Rate-limited; retry after ${err.retryAfter ?? "a few"}s.`;
|
|
516
|
+
if (err instanceof ValidationError) return `Invalid input: ${err.detail}.`;
|
|
517
|
+
if (err instanceof SchemaValidationError) return `Invalid input: ${err.message}.`;
|
|
518
|
+
if (err instanceof RineTimeoutError) return "Request timed out; try again or raise the timeout.";
|
|
519
|
+
if (err instanceof ConfigError) return String(err.message);
|
|
520
|
+
if (err instanceof CryptoError) return `Encryption error: ${err.message} (check your agent keys are on disk).`;
|
|
521
|
+
if (err instanceof APIConnectionError) return `Could not reach rine: ${err.message}.`;
|
|
522
|
+
if (err instanceof RineApiError) return `${err.status}: ${err.detail}`;
|
|
523
|
+
const message = String(messageOf(err));
|
|
524
|
+
if (isGroupNotFound(message)) return groupNotFoundMessage(message);
|
|
525
|
+
return message;
|
|
526
|
+
}
|
|
527
|
+
function messageOf(err) {
|
|
528
|
+
if (err instanceof Error) return err.message;
|
|
529
|
+
return String(err ?? "unknown error");
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/tool.ts
|
|
533
|
+
/**
|
|
534
|
+
* Shared `defineTool` plumbing every rine tool reuses (no duplicated logic):
|
|
535
|
+
* env-based identity resolution and the try/catch → `formatError` wrapper
|
|
536
|
+
* (errors→strings, never a reject).
|
|
537
|
+
*
|
|
538
|
+
* Unlike the Mastra integration (which threads identity through a `RequestContext`),
|
|
539
|
+
* Eve tools run in the app runtime with full `process.env` access, so identity is
|
|
540
|
+
* resolved straight from the environment via `getRineClient` — no per-call context
|
|
541
|
+
* plumbing, no credentials in the model-visible input schema.
|
|
542
|
+
*
|
|
543
|
+
* A tool's per-tool body is just `(client, input, apiUrl) => string`; this helper
|
|
544
|
+
* supplies the surrounding contract. The concrete `defineTool({...})` (with the
|
|
545
|
+
* model-facing description + schemas) lives in each domain module; this file only
|
|
546
|
+
* builds the `execute` closure and the shared option/body types.
|
|
547
|
+
*/
|
|
548
|
+
/**
|
|
549
|
+
* Resolve {@link RineToolOpts.needsApproval} to an Eve `needsApproval` callback,
|
|
550
|
+
* or `undefined` (no gate) when unset. Only the mutating tool factories apply it.
|
|
551
|
+
*/
|
|
552
|
+
function approvalGate(opts) {
|
|
553
|
+
switch (opts.needsApproval) {
|
|
554
|
+
case "always": return always();
|
|
555
|
+
case "once": return once();
|
|
556
|
+
case "never": return never();
|
|
557
|
+
default: return;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** Resolve the effective `AsyncRineClient` for one `execute` call (lazy). */
|
|
561
|
+
function resolveClient(opts) {
|
|
562
|
+
if (opts.client) {
|
|
563
|
+
const agent = actingAgent(opts);
|
|
564
|
+
return agent ? opts.client.withAgent(agent) : opts.client;
|
|
565
|
+
}
|
|
566
|
+
return getRineClient({
|
|
567
|
+
configDir: opts.configDir,
|
|
568
|
+
apiUrl: opts.apiUrl,
|
|
569
|
+
agent: actingAgent(opts)
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* The same resolved `apiUrl` the client is built against (factory override →
|
|
574
|
+
* `RINE_API_URL` env → `resolveApiUrl()`). Exposed so handle→UUID pre-resolution
|
|
575
|
+
* hits the SAME server WebFinger the client uses.
|
|
576
|
+
*/
|
|
577
|
+
function resolveApiUrlFor(opts) {
|
|
578
|
+
return opts.apiUrl ?? resolveApiUrl();
|
|
579
|
+
}
|
|
580
|
+
/** Brand a model-supplied recipient string for the SDK send surface. */
|
|
581
|
+
function asRecipient(to) {
|
|
582
|
+
return to;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Belt-and-suspenders ciphertext redactor for `read` / `inbox` / `thread`.
|
|
586
|
+
* The renderers + `outputSchema: z.string()` already guarantee the `execute`
|
|
587
|
+
* return is redacted text; this coerces ANY value to plain text before it can
|
|
588
|
+
* reach the model, in Eve's `ToolModelOutput` shape.
|
|
589
|
+
*/
|
|
590
|
+
function redactToText(output) {
|
|
591
|
+
return {
|
|
592
|
+
type: "text",
|
|
593
|
+
value: typeof output === "string" ? output : ""
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Build an Eve-tool `execute(input, ctx)` from a zod schema + body, applying:
|
|
598
|
+
* 1. zod `.parse(raw)` of the model input (defaults + coercion + validation),
|
|
599
|
+
* 2. the lazy env-resolved client,
|
|
600
|
+
* 3. the try/catch→formatError contract (resolves a string, never rejects).
|
|
601
|
+
*
|
|
602
|
+
* Eve hands `execute` the raw model input as `Record<string, unknown>`; we own the
|
|
603
|
+
* parse because Eve receives a JSON Schema (not the zod schema) and may not apply
|
|
604
|
+
* zod defaults. The Eve `ToolContext` is intentionally ignored — rine identity is
|
|
605
|
+
* environment-resolved, not session-scoped.
|
|
606
|
+
*/
|
|
607
|
+
function makeExecute(schema, opts, body) {
|
|
608
|
+
return async (raw) => {
|
|
609
|
+
try {
|
|
610
|
+
const input = schema.parse(raw);
|
|
611
|
+
return await body(resolveClient(opts), input, resolveApiUrlFor(opts));
|
|
612
|
+
} catch (err) {
|
|
613
|
+
return formatError(err);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
export { renderRoster as C, renderGroups as S, renderThreadLine as _, GROUP_ON_WAIT_MESSAGE as a, verifiedNote as b, renderDiscover as c, renderInvites as d, renderJoinResult as f, renderThread as g, renderSingleMessage as h, redactToText as i, renderGroup as l, renderProfile as m, asRecipient as n, formatError as o, renderMessageBody as p, makeExecute as r, isGroupUnsupportedOnWait as s, approvalGate as t, renderInbox as u, renderWhoAmI as v, renderGroupSummaries as x, senderLabel as y };
|
package/dist/tool.d.ts
CHANGED
|
@@ -57,14 +57,14 @@ export declare function resolveClient(opts: RineToolOpts): AsyncRineClient;
|
|
|
57
57
|
export declare function resolveApiUrlFor(opts: RineToolOpts): string;
|
|
58
58
|
/**
|
|
59
59
|
* The recipient union `client.send`/`sendAndWait` accept. The model supplies a
|
|
60
|
-
* free-form `to` string (a `name@org` handle, a `#
|
|
60
|
+
* free-form `to` string (a `name@org` handle, a `#logistics@acme.rine.network` handle, or a UUID);
|
|
61
61
|
* the SDK normalizes + validates it at runtime, so this brands the string once.
|
|
62
62
|
*/
|
|
63
63
|
type Recipient = AgentHandle | AgentUuid | GroupHandle | GroupUuid;
|
|
64
64
|
/** Brand a model-supplied recipient string for the SDK send surface. */
|
|
65
65
|
export declare function asRecipient(to: string): Recipient;
|
|
66
66
|
/**
|
|
67
|
-
* Belt-and-suspenders ciphertext redactor for `read` / `
|
|
67
|
+
* Belt-and-suspenders ciphertext redactor for `read` / `inbox` / `thread`.
|
|
68
68
|
* The renderers + `outputSchema: z.string()` already guarantee the `execute`
|
|
69
69
|
* return is redacted text; this coerces ANY value to plain text before it can
|
|
70
70
|
* reach the model, in Eve's `ToolModelOutput` shape.
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* unauthenticated directory reads
|
|
2
|
+
* The 4 discovery tool factories. `rine_discover` and `rine_inspect` are
|
|
3
|
+
* unauthenticated directory reads; `rine_whoami` reads this agent's OWN
|
|
4
|
+
* identity, so it is the one authenticated verb here.
|
|
5
|
+
*
|
|
6
|
+
* `rine_discover_groups` is the directory's group half and lives with the other
|
|
7
|
+
* group readers in `groups-list.ts`; only its registry entry says `discovery`.
|
|
4
8
|
*/
|
|
5
9
|
import { type RineToolOpts } from "../tool.js";
|
|
6
10
|
/** `rine_discover` — search the public agent directory. */
|
|
7
11
|
export declare function rineDiscoverTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
|
|
8
12
|
/** `rine_inspect` — fetch one agent's full public profile. */
|
|
9
13
|
export declare function rineInspectTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
|
|
14
|
+
/** `rine_whoami` — this agent's own org, trust tier, and live handles. */
|
|
15
|
+
export declare function rineWhoamiTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
|