@segmentstream/agent 0.5.4 → 0.6.4
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 +78 -18
- package/dist/extension/connections/ontology.mjs +2 -2
- package/dist/extension/instructions.md +10 -9
- package/dist/extension/lib/attachment-tools.mjs +1 -3
- package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
- package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
- package/dist/extension/lib/confirmation-description.d.ts +2 -0
- package/dist/extension/lib/confirmations.d.ts +5 -0
- package/dist/extension/lib/confirmations.mjs +25 -5
- package/dist/extension/lib/conversations.d.ts +12 -1
- package/dist/extension/lib/conversations.mjs +4 -3
- package/dist/extension/lib/ontology-connection.d.ts +4 -0
- package/dist/extension/lib/ontology-connection.mjs +46 -3
- package/dist/extension/lib/ontology-tools.d.ts +1 -0
- package/dist/extension/lib/ontology-tools.mjs +27 -2
- package/dist/extension/lib/telegram/adapter.d.ts +1 -1
- package/dist/extension/lib/telegram/adapter.mjs +18 -28
- package/dist/extension/lib/telegram/attachments-send.d.ts +7 -3
- package/dist/extension/lib/telegram/attachments-send.mjs +25 -7
- package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
- package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
- package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
- package/dist/extension/lib/telegram/confirmations.mjs +140 -21
- package/dist/extension/lib/telegram/conversations.mjs +5 -3
- package/dist/extension/lib/telegram/customer-text.d.ts +13 -1
- package/dist/extension/lib/telegram/customer-text.mjs +108 -7
- package/dist/extension/lib/telegram/format.d.ts +17 -0
- package/dist/extension/lib/telegram/format.mjs +171 -0
- package/dist/extension/lib/telegram/health.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
- package/dist/extension/lib/telegram/proxy-fetch.d.ts +1 -1
- package/dist/extension/lib/telegram/proxy-fetch.mjs +14 -6
- package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
- package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
- package/dist/extension/lib/telegram/send-queue.d.ts +4 -0
- package/dist/extension/lib/telegram/send-queue.mjs +35 -0
- package/dist/extension/lib/telegram/silent.d.ts +3 -3
- package/dist/extension/lib/telegram/silent.mjs +19 -4
- package/package.json +2 -1
- package/dist/extension/lib/telegram/requests.d.ts +0 -80
- package/dist/extension/lib/telegram/requests.mjs +0 -186
package/README.md
CHANGED
|
@@ -98,7 +98,6 @@ platform's deployment manifest.
|
|
|
98
98
|
| ------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
99
|
| `segmentstream__ontology` | `connections` | The app's `/mcp`, resolved per turn for the turn's member. No member, no connection. |
|
|
100
100
|
| `segmentstream__telegram` | `channels` | `POST /eve/v1/telegram`: Telegram through the connection, with pairing, routing and voice. |
|
|
101
|
-
| | | `POST /eve/v1/telegram/messages`: requests from the ontology's `agent_chat.send_message`. |
|
|
102
101
|
| `segmentstream__attachments_list`, `_read`, `_promote`, `_discard` | `tools` | The member's temporary attachments in this chat: list, read, keep through an action, drop. |
|
|
103
102
|
| `segmentstream__attachments_send` | `tools` | Sends readable stored ontology files into the current Telegram chat and topic. |
|
|
104
103
|
| platform rules | `instructions` | Changes go through actions; `describe` indexes or inspects refs; `discover` searches metadata; tools are already narrowed; when to answer `[SILENT]`; how to handle attachments |
|
|
@@ -197,18 +196,16 @@ share a group chat's session never share a client.
|
|
|
197
196
|
`migrate_from_chat_id` in the new one) reports the new id with `migratedFrom: <old id>`, starts
|
|
198
197
|
no turn and drops the old chat from the memo. The `<chat>` block carries at most 1000
|
|
199
198
|
characters of `whenToUse`.
|
|
200
|
-
- **
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
production, 404 without `channels.telegram`, 401 for a missing or invalid token and 400 for a
|
|
211
|
-
body that does not match, including a request over 4000 characters.
|
|
199
|
+
- **Cross-chat drafts.** The agent uses `chat_post_message` through its existing ontology
|
|
200
|
+
connection. The exact full text, destination and authenticated attribution appear on a deterministic
|
|
201
|
+
Confirm / Edit / Cancel card in the requesting group or DM. Only the requester can use it; the
|
|
202
|
+
card expires after ten minutes. Edit cancels the original call and asks for a revision. Oversize
|
|
203
|
+
previews are refused, and uncertain delivery is never automatically retried. Sent receipts retain
|
|
204
|
+
the preview and show a Telegram link when available; archive failure is distinct from send failure.
|
|
205
|
+
Agent SDK 0.6.0 prepares this flow; delivery becomes available when the ontology advertises the
|
|
206
|
+
protocol below. The old instruction receiver `/eve/v1/telegram/messages` is removed. Telegram
|
|
207
|
+
routes now serve only the inbound webhook and authenticated health; old instruction relay calls
|
|
208
|
+
fail instead of starting an agent turn.
|
|
212
209
|
- **Silence.** When the model answers a routed message with exactly `[SILENT]` (or `NO_REPLY`), in
|
|
213
210
|
any case, nothing is sent to the chat. Empty and whitespace-only final text also send nothing.
|
|
214
211
|
The turn stays in the session. Archive decisions distinguish silence markers, empty text and
|
|
@@ -312,10 +309,32 @@ JSON envelope and limits the decoded body to **2 MiB including multipart overhea
|
|
|
312
309
|
not stream uploads. Files whose multipart body exceeds this limit return `too_large`; the
|
|
313
310
|
larger Telegram limits do not bypass it. Downloads are bounded even if storage understates size.
|
|
314
311
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
Exact chat posting accepts text only through the ontology's `chat_post_message`.
|
|
313
|
+
There are no `document_ids` relay arguments, `AgentMessage` transport or
|
|
314
|
+
`<agent_request>` turns. Stored file tools keep their member authorization in the
|
|
315
|
+
current conversation; group purpose and document restrictions still apply.
|
|
316
|
+
|
|
317
|
+
## Telegram answer delivery
|
|
318
|
+
|
|
319
|
+
Agent replies render Markdown as visible text and Telegram entities. Headings, emphasis,
|
|
320
|
+
links, lists, quotations and code are supported; decorative separator lines are removed.
|
|
321
|
+
The customer-text guard checks the rendered prose and link destinations. If rendering fails,
|
|
322
|
+
delivery uses clean plain text. Only an explicit Telegram entity-formatting refusal permits
|
|
323
|
+
a plain-text retry; timeouts and other uncertain outcomes must never trigger an automatic resend.
|
|
324
|
+
Attachment captions use the same rendering and fallback rules.
|
|
325
|
+
|
|
326
|
+
Replies split at paragraph, line, sentence, word or UTF-16 boundaries within Telegram's 4096-unit
|
|
327
|
+
limit. Entities are rebased or clipped on each part, and surrogate pairs stay intact. The whole
|
|
328
|
+
reply or attachment batch occupies one shared FIFO slot per workspace/connection/chat, including
|
|
329
|
+
all forum topics. A failed slot releases the next one. Sends require a positive Telegram message
|
|
330
|
+
id; missing acknowledgements are errors and archive as unknown. Partial-delivery errors retain
|
|
331
|
+
the message ids already acknowledged.
|
|
332
|
+
|
|
333
|
+
This queue lives in one SDK runtime. It does not coordinate separate serverless instances or
|
|
334
|
+
the ontology service's exact `chat_post_message` provider. Ordering all of those together needs
|
|
335
|
+
a shared provider sequencer. Exact posting retains its approved literal text and durable receipt;
|
|
336
|
+
this formatter does not rewrite approved posts or their confirmation cards. The removed
|
|
337
|
+
`agent_chat.send_message` instruction relay must not be restored as a delivery fallback.
|
|
319
338
|
|
|
320
339
|
## Local development
|
|
321
340
|
|
|
@@ -362,8 +381,49 @@ attachment references. It reloads stored context for the same chat/topic each tu
|
|
|
362
381
|
across cold starts and compaction. File bytes and tool output bodies are not archived.
|
|
363
382
|
|
|
364
383
|
Retention defaults to 365 days per workspace. Platform REST reads, the workspace ontology's
|
|
365
|
-
`
|
|
384
|
+
`chat_history` and `chat_search`, and the member REST facade enforce workspace/group
|
|
366
385
|
permissions and DM participant/admin scope. Deploy the backend migration/API and upgrade the
|
|
367
386
|
ontology SDK along with this extension. See the repository's
|
|
368
387
|
[conversation storage guide](../../docs/agent-conversation-storage.md) for APIs, retention,
|
|
369
388
|
eve's existing storage, and Telegram's delivery limits.
|
|
389
|
+
|
|
390
|
+
### Exact chat posting protocol (prepared in agent SDK 0.6.0)
|
|
391
|
+
|
|
392
|
+
An authenticated `tools/list` advertises `_meta: { "segmentstream/chat-post": 1 }` on
|
|
393
|
+
`chat_post_message`. Without this marker,
|
|
394
|
+
ordinary tools use the compatible connection and no runtime arguments are added. Posting with
|
|
395
|
+
missing/incomplete preview metadata is denied. The versioned connection has its own instance key.
|
|
396
|
+
|
|
397
|
+
The runtime reads `me` for the server-derived `attribution`, and `chat_list({ chat })` for exactly
|
|
398
|
+
one eligible destination: `{ items: [{ id, title, provider, external_id }], next_cursor: null }`.
|
|
399
|
+
Provider is currently `telegram`. The final plain text is exactly `attribution + ": " + text`;
|
|
400
|
+
`reply_to` is optional. A preview digest is SHA-256 of the JSON array of chat ID, provider,
|
|
401
|
+
external ID, title, attribution, text, final text and nullable reply target, in that order.
|
|
402
|
+
The target/attribution are resolved again immediately before executing the frozen call.
|
|
403
|
+
|
|
404
|
+
Eve supplies the model-hidden `_segmentstream` argument on **all** calls of a negotiated connection.
|
|
405
|
+
It contains `version: 1`, `session_id` and `call_id`. A consumed, unexpired chat approval additionally
|
|
406
|
+
supplies `operation_id`, source provider/chat/topic coordinates and `expected: { chat_id, digest }`.
|
|
407
|
+
The operation ID is `hq:` plus SHA-256 of the JSON array `[session_id, call_id]`; it stays stable
|
|
408
|
+
on replay and changes for a revision. The model cannot provide an idempotency key or this envelope.
|
|
409
|
+
The ontology must authenticate runtime provenance, consume/remove the envelope before strict input
|
|
410
|
+
validation for ordinary tools, derive the posting key from the operation, and recompute attribution.
|
|
411
|
+
External OAuth/service callers still supply their own `idempotency_key` and cannot use this envelope.
|
|
412
|
+
These server changes belong to the later ontology stage; do not advertise the marker prematurely.
|
|
413
|
+
|
|
414
|
+
Source previews, accepted decisions and receipts use existing archive events with operation-linked
|
|
415
|
+
`toolCallId`/event IDs. A cancelled Edit carries its pending revision question in the authoritative
|
|
416
|
+
Eve resolution, so member/topic routing survives a cold start. No second provider sender lives here.
|
|
417
|
+
|
|
418
|
+
## Release and migration
|
|
419
|
+
|
|
420
|
+
The tested release uses agent SDK 0.6.4, Eve 0.76.3 and ontology SDK 0.28.3.
|
|
421
|
+
Deploy the agent first, then backend archive migration 0033/API, then the ontology
|
|
422
|
+
with its declared Telegram provider mapping and explicit `agent_chat:post_message`
|
|
423
|
+
grant. Older ontologies keep ordinary tools working without the extra envelope.
|
|
424
|
+
An older archive returning only `{stored:true}` cannot authorize a send.
|
|
425
|
+
|
|
426
|
+
Rollback
|
|
427
|
+
disables posting while preserving archive claims and deduplication tombstones;
|
|
428
|
+
it never restores relay or retries uncertain deliveries.
|
|
429
|
+
See [migration and verification](../../docs/agent-chat-delivery-migration.md).
|
|
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import segmentstream from "../extension.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { ontologyConnectionForTurn } from "../lib/ontology-connection.mjs";
|
|
9
9
|
import { defineDynamic } from "eve/connections";
|
|
10
|
-
var ontology_default = defineDynamic({ events: { "turn.started": (_event, context) => segmentstream.config.ontology.connection ?
|
|
10
|
+
var ontology_default = defineDynamic({ events: { "turn.started": (_event, context) => segmentstream.config.ontology.connection ? ontologyConnectionForTurn({ principal: context.session.auth.current }) : null } });
|
|
11
11
|
export { ontology_default as default };
|
|
@@ -28,15 +28,16 @@ talking to you, and you act with exactly their permissions.
|
|
|
28
28
|
- Never invent records or values. Say when the ontology does not hold what was asked.
|
|
29
29
|
- When you have been routed a message but a reply would add nothing, answer with exactly
|
|
30
30
|
`[SILENT]` and nothing else. Use it only in that case; it suppresses delivery to the chat.
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
- To post in another chat, find the destination with `chat_list` and colleagues with `user_find`.
|
|
32
|
+
Read `chat_history` or `chat_search` only when permitted, then compose the exact draft and call
|
|
33
|
+
`chat_post_message`. The runtime supplies the operation key. Its Confirm / Edit / Cancel card
|
|
34
|
+
shows the full attributed text in the requesting chat; only the requester can act on it and it
|
|
35
|
+
expires after ten minutes. Text such as "yes" or "да" cannot approve a post. After Edit, ask what
|
|
36
|
+
to change and make a new call/card for the revision; the cancelled call cannot be reused.
|
|
37
|
+
An uncertain delivery must never be automatically resent, including through a fresh call/key.
|
|
38
|
+
For a reply in the current chat, answer normally. If chat posting is unavailable, say so.
|
|
39
|
+
Never call the removed instruction relay or any agent invocation/self-invocation tool. A post must not start an agent.
|
|
40
|
+
The existing `agent_chat` registry records group purpose and usage; update those only when permitted.
|
|
40
41
|
- Send stored ontology documents with `segmentstream__attachments_send`: give `documentIds`
|
|
41
42
|
(1 to 10 document record ids), or `{ entity, id, property? }` references for other records with
|
|
42
43
|
file properties. The tool derives the member, Telegram chat and topic from this turn, checks
|
|
@@ -17,9 +17,7 @@ var AttachmentOwnerError = class extends Error {
|
|
|
17
17
|
name = "AttachmentOwnerError";
|
|
18
18
|
};
|
|
19
19
|
function attachmentOwner(input) {
|
|
20
|
-
const
|
|
21
|
-
const requesterId = attributes?.requesterKind === "member" && attributes.chatType === "group" ? attributes.requesterId : void 0;
|
|
22
|
-
const memberId = memberIdOf(input.principal) ?? (typeof requesterId === "string" ? requesterId : void 0);
|
|
20
|
+
const memberId = memberIdOf(input.principal);
|
|
23
21
|
const chatId = input.principal?.attributes.chatId;
|
|
24
22
|
if (memberId === void 0 || memberId === "" || typeof chatId !== "string" || chatId === "") throw new AttachmentOwnerError("Attachments require a workspace member in a chat with the agent.");
|
|
25
23
|
return {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConfirmationDescription } from "./confirmation-description.js";
|
|
2
|
+
/** Versioned agreement with the authenticated ontology, not a model supplied capability. */
|
|
3
|
+
export declare const CHAT_POST_PROTOCOL = "segmentstream/chat-post";
|
|
4
|
+
export declare const CHAT_POST_ENVELOPE = "_segmentstream";
|
|
5
|
+
export interface ChatPostPreview {
|
|
6
|
+
readonly chatId: string;
|
|
7
|
+
readonly provider: "telegram";
|
|
8
|
+
readonly externalId: string;
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly attribution: string;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly finalText: string;
|
|
13
|
+
readonly replyTo: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function isChatPost(action: string): boolean;
|
|
16
|
+
export declare function supportsChatPost(tool: Record<string, unknown>): boolean;
|
|
17
|
+
export declare function chatPostDigest(preview: ChatPostPreview): string;
|
|
18
|
+
export declare function chatPostOperationId(sessionId: string, callId: string): string;
|
|
19
|
+
export declare function describeChatPost(input: {
|
|
20
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
21
|
+
readonly caller: Record<string, unknown>;
|
|
22
|
+
readonly destinations: Record<string, unknown>;
|
|
23
|
+
}): ConfirmationDescription;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { object } from "./confirmation-description.mjs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
const CHAT_POST_PROTOCOL = "segmentstream/chat-post";
|
|
10
|
+
const CHAT_POST_ENVELOPE = "_segmentstream";
|
|
11
|
+
function isChatPost(action) {
|
|
12
|
+
return action.split("__").at(-1) === "chat_post_message";
|
|
13
|
+
}
|
|
14
|
+
function supportsChatPost(tool) {
|
|
15
|
+
return isChatPost(String(tool.name)) && object(tool._meta)["segmentstream/chat-post"] === 1;
|
|
16
|
+
}
|
|
17
|
+
function chatPostDigest(preview) {
|
|
18
|
+
return createHash("sha256").update(JSON.stringify([
|
|
19
|
+
preview.chatId,
|
|
20
|
+
preview.provider,
|
|
21
|
+
preview.externalId,
|
|
22
|
+
preview.title,
|
|
23
|
+
preview.attribution,
|
|
24
|
+
preview.text,
|
|
25
|
+
preview.finalText,
|
|
26
|
+
preview.replyTo
|
|
27
|
+
])).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
function chatPostOperationId(sessionId, callId) {
|
|
30
|
+
return `hq:${createHash("sha256").update(JSON.stringify([sessionId, callId])).digest("hex")}`;
|
|
31
|
+
}
|
|
32
|
+
function describeChatPost(input) {
|
|
33
|
+
const { values, caller, destinations } = input;
|
|
34
|
+
const items = Array.isArray(destinations.items) ? destinations.items.map(object) : [];
|
|
35
|
+
const target = items[0];
|
|
36
|
+
if (Object.keys(values).some((key) => ![
|
|
37
|
+
"chat",
|
|
38
|
+
"text",
|
|
39
|
+
"reply_to"
|
|
40
|
+
].includes(key)) || typeof values.chat !== "string" || !values.chat || typeof values.text !== "string" || !values.text.trim() || values.reply_to !== void 0 && (typeof values.reply_to !== "string" || !values.reply_to) || items.length !== 1 || destinations.next_cursor != null || destinations.nextCursor != null || target === void 0 || ![
|
|
41
|
+
target.id,
|
|
42
|
+
target.title,
|
|
43
|
+
target.external_id,
|
|
44
|
+
caller.attribution
|
|
45
|
+
].every((value) => typeof value === "string" && value.length > 0) || target.provider !== "telegram" || target.id !== values.chat && target.title !== values.chat) throw new Error("An unambiguous destination and a complete exact chat preview are required.");
|
|
46
|
+
return {
|
|
47
|
+
action: "Post message",
|
|
48
|
+
fields: [],
|
|
49
|
+
chatPost: {
|
|
50
|
+
chatId: target.id,
|
|
51
|
+
provider: "telegram",
|
|
52
|
+
externalId: target.external_id,
|
|
53
|
+
title: target.title,
|
|
54
|
+
attribution: caller.attribution,
|
|
55
|
+
text: values.text,
|
|
56
|
+
finalText: `${caller.attribution}: ${values.text}`,
|
|
57
|
+
replyTo: typeof values.reply_to === "string" ? values.reply_to : null
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export { CHAT_POST_ENVELOPE, CHAT_POST_PROTOCOL, chatPostDigest, chatPostOperationId, describeChatPost, isChatPost, supportsChatPost };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatPostPreview } from "./chat-post-confirmation.js";
|
|
1
2
|
/** Only human-facing facts cross into the confirmation renderer; identifiers stay in the gate. */
|
|
2
3
|
export interface ConfirmationDescription {
|
|
3
4
|
readonly action: string;
|
|
@@ -7,6 +8,7 @@ export interface ConfirmationDescription {
|
|
|
7
8
|
readonly label: string;
|
|
8
9
|
readonly value: string;
|
|
9
10
|
}[];
|
|
11
|
+
readonly chatPost?: ChatPostPreview;
|
|
10
12
|
}
|
|
11
13
|
export declare function object(value: unknown): Record<string, unknown>;
|
|
12
14
|
export declare function humanize(value: string): string;
|
|
@@ -19,6 +19,11 @@ export interface Confirmation {
|
|
|
19
19
|
readonly expiresAt: number;
|
|
20
20
|
readonly status: "pending" | "approved" | "consumed" | "cancelled";
|
|
21
21
|
readonly description?: ConfirmationDescription;
|
|
22
|
+
readonly source?: {
|
|
23
|
+
readonly chatId: string;
|
|
24
|
+
readonly threadId: string;
|
|
25
|
+
};
|
|
26
|
+
readonly cancellationReason?: "edit" | "expired";
|
|
22
27
|
}
|
|
23
28
|
export declare const confirmations: StateHandle<Record<string, Confirmation>>;
|
|
24
29
|
/** Eve pauses the original call and rechecks this policy immediately before executing it. */
|
|
@@ -5,6 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { memberIdOf } from "./telegram/principal.mjs";
|
|
8
|
+
import { chatPostDigest, isChatPost } from "./chat-post-confirmation.mjs";
|
|
9
|
+
import { chatPostCard } from "./telegram/chat-post-card.mjs";
|
|
8
10
|
import { createHash } from "node:crypto";
|
|
9
11
|
import { defineState } from "eve/context";
|
|
10
12
|
const CONFIRMATION_TTL_MS = 6e5;
|
|
@@ -47,6 +49,20 @@ function actionApproval(options) {
|
|
|
47
49
|
console.warn("The ontology action permission check failed.", error);
|
|
48
50
|
return denied("The action is unavailable or the member no longer has permission.");
|
|
49
51
|
}
|
|
52
|
+
const posting = isChatPost(ctx.toolName);
|
|
53
|
+
let description;
|
|
54
|
+
if (posting) try {
|
|
55
|
+
description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
|
|
56
|
+
const preview = description?.chatPost;
|
|
57
|
+
const source = ctx.session.auth.current?.attributes;
|
|
58
|
+
if (preview === void 0 || typeof source?.chatId !== "string" || source.chatId === preview.externalId) return denied("A complete preview for a different chat is required. Reply normally in this chat.");
|
|
59
|
+
chatPostCard(preview, "en");
|
|
60
|
+
chatPostCard(preview, "ru");
|
|
61
|
+
if (saved !== void 0 && (saved.description?.chatPost === void 0 || chatPostDigest(saved.description.chatPost) !== chatPostDigest(preview) || saved.source?.chatId !== source.chatId || saved.source.threadId !== String(source.messageThreadId ?? ""))) return denied("The destination or attribution changed. Make a new call and confirmation.");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn("The exact chat confirmation could not be prepared.", error);
|
|
64
|
+
return denied("The full exact chat preview is unavailable or too long. Make a new call.");
|
|
65
|
+
}
|
|
50
66
|
if (saved?.status === "approved") {
|
|
51
67
|
state.update((all) => ({
|
|
52
68
|
...all,
|
|
@@ -58,10 +74,9 @@ function actionApproval(options) {
|
|
|
58
74
|
return "approved";
|
|
59
75
|
}
|
|
60
76
|
if (saved !== void 0) return denied("The original call is waiting for its Confirm / Cancel buttons.");
|
|
61
|
-
if (!needsConfirmation(labels)) return "approved";
|
|
62
|
-
let description;
|
|
77
|
+
if (!posting && !needsConfirmation(labels)) return "approved";
|
|
63
78
|
try {
|
|
64
|
-
description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
|
|
79
|
+
if (!posting) description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
|
|
65
80
|
} catch (error) {
|
|
66
81
|
console.warn("The confirmation description could not be read.", error);
|
|
67
82
|
}
|
|
@@ -73,7 +88,11 @@ function actionApproval(options) {
|
|
|
73
88
|
inputHash: hash,
|
|
74
89
|
expiresAt: now() + CONFIRMATION_TTL_MS,
|
|
75
90
|
status: "pending",
|
|
76
|
-
...description === void 0 ? {} : { description }
|
|
91
|
+
...description === void 0 ? {} : { description },
|
|
92
|
+
...posting ? { source: {
|
|
93
|
+
chatId: String(ctx.session.auth.current?.attributes.chatId),
|
|
94
|
+
threadId: String(ctx.session.auth.current?.attributes.messageThreadId ?? "")
|
|
95
|
+
} } : {}
|
|
77
96
|
}
|
|
78
97
|
}));
|
|
79
98
|
return "user-approval";
|
|
@@ -93,7 +112,8 @@ function actionApproval(options) {
|
|
|
93
112
|
...all,
|
|
94
113
|
[ctx.request.callId]: {
|
|
95
114
|
...saved,
|
|
96
|
-
status: ctx.response.decision === "approve" ? "approved" : "cancelled"
|
|
115
|
+
status: ctx.response.decision === "approve" ? "approved" : "cancelled",
|
|
116
|
+
...ctx.response.decision === "cancel" && ["edit", "expired"].includes(String(ctx.response.principal.attributes.confirmationReason)) ? { cancellationReason: ctx.response.principal.attributes.confirmationReason } : {}
|
|
97
117
|
}
|
|
98
118
|
}));
|
|
99
119
|
return { status: "allowed" };
|
|
@@ -22,7 +22,18 @@ export interface ConversationEvent {
|
|
|
22
22
|
readonly toolCallId: string | null;
|
|
23
23
|
readonly toolName: string | null;
|
|
24
24
|
readonly argsSummary: string | null;
|
|
25
|
-
readonly status: "requested" | "completed" | "failed" | "rejected" | "cancelled" | "unknown" | null;
|
|
25
|
+
readonly status: "requested" | "completed" | "failed" | "rejected" | "cancelled" | "unknown" | "sent" | "uncertain" | null;
|
|
26
|
+
readonly operationId?: string | null;
|
|
27
|
+
readonly requestedBy?: {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly kind: "member" | "service";
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly via: "oauth" | "hq" | "service";
|
|
32
|
+
readonly clientId: string | null;
|
|
33
|
+
readonly clientName: string | null;
|
|
34
|
+
readonly attribution: string;
|
|
35
|
+
} | null;
|
|
36
|
+
readonly url?: string | null;
|
|
26
37
|
readonly attachments: ReadonlyArray<{
|
|
27
38
|
readonly id: string;
|
|
28
39
|
readonly kind: string;
|
|
@@ -9,11 +9,12 @@ import { runtimeToken } from "./runtime-token.mjs";
|
|
|
9
9
|
function collapseConversationEvents(events) {
|
|
10
10
|
const latest = new Map();
|
|
11
11
|
for (const event of events) {
|
|
12
|
-
const key = event.kind === "inbound" && event.provider === "telegram" && event.messageId !== null ? `telegram:${event.chatId}:${event.messageId}:message` : event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
|
|
12
|
+
const key = event.kind === "outbound" && event.operationId != null ? `chat-delivery:${event.operationId}` : event.kind === "inbound" && event.provider === "telegram" && event.messageId !== null ? `telegram:${event.chatId}:${event.messageId}:message` : event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
|
|
13
13
|
const previous = latest.get(key);
|
|
14
14
|
if (previous === void 0 || (/^\d+$/u.test(event.sequence) && /^\d+$/u.test(previous.sequence) ? BigInt(event.sequence) > BigInt(previous.sequence) : event.storedAt > previous.storedAt)) latest.set(key, {
|
|
15
15
|
...event,
|
|
16
|
-
eventId: key
|
|
16
|
+
eventId: key,
|
|
17
|
+
...event.kind === "outbound" && event.operationId != null && event.status === "requested" ? { status: "uncertain" } : {}
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
return [...latest.values()];
|
|
@@ -95,7 +96,7 @@ async function storedConversationContext(archive, coordinates) {
|
|
|
95
96
|
chatId: coordinates.chatId,
|
|
96
97
|
...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
|
|
97
98
|
limit: 100
|
|
98
|
-
})).events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && event.status
|
|
99
|
+
})).events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && ["completed", "sent"].includes(event.status ?? ""));
|
|
99
100
|
const selected = [];
|
|
100
101
|
let characters = 0;
|
|
101
102
|
for (const event of messages) {
|
|
@@ -34,4 +34,8 @@ export declare function ontologyConnection(input: {
|
|
|
34
34
|
readonly environment?: Environment;
|
|
35
35
|
readonly token?: RuntimeToken;
|
|
36
36
|
readonly fetch?: typeof fetch;
|
|
37
|
+
/** Set only by the authenticated catalog handshake, never by model input. */
|
|
38
|
+
readonly chatPosting?: boolean;
|
|
37
39
|
}): McpClientConnectionDefinition | null;
|
|
40
|
+
/** Negotiate before opening Eve's client: old strict schemas never receive the new envelope. */
|
|
41
|
+
export declare function ontologyConnectionForTurn(input: Parameters<typeof ontologyConnection>[0]): Promise<McpClientConnectionDefinition | null>;
|
|
@@ -6,7 +6,8 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { appOrigin } from "./environment.mjs";
|
|
8
8
|
import { memberIdOf } from "./telegram/principal.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { CHAT_POST_ENVELOPE, chatPostDigest, chatPostOperationId, isChatPost } from "./chat-post-confirmation.mjs";
|
|
10
|
+
import { actionApproval, confirmations } from "./confirmations.mjs";
|
|
10
11
|
import { runtimeToken, tokenExpiry } from "./runtime-token.mjs";
|
|
11
12
|
import { ontologyTools } from "./ontology-tools.mjs";
|
|
12
13
|
import { defineMcpClientConnection } from "eve/connections";
|
|
@@ -29,8 +30,36 @@ function ontologyConnection(input) {
|
|
|
29
30
|
return defineMcpClientConnection({
|
|
30
31
|
url: `${origin}/mcp`,
|
|
31
32
|
description: DESCRIPTION,
|
|
32
|
-
instanceKey: `${origin}/mcp#member:${memberId}`,
|
|
33
|
+
instanceKey: `${origin}/mcp#member:${memberId}${input.chatPosting ? ":chat-post-v1" : ""}`,
|
|
33
34
|
approval: actionApproval(ontologyTools(input)),
|
|
35
|
+
...input.chatPosting ? { toolCall: { providedArguments: { [CHAT_POST_ENVELOPE]: (ctx) => {
|
|
36
|
+
actAsHeaders({
|
|
37
|
+
principal: ctx.session.auth.current,
|
|
38
|
+
expectedMemberId: memberId
|
|
39
|
+
});
|
|
40
|
+
const base = {
|
|
41
|
+
version: 1,
|
|
42
|
+
session_id: ctx.session.id,
|
|
43
|
+
call_id: ctx.callId
|
|
44
|
+
};
|
|
45
|
+
if (!isChatPost(ctx.toolName)) return base;
|
|
46
|
+
const saved = confirmations.get()[ctx.callId];
|
|
47
|
+
const preview = saved?.description?.chatPost;
|
|
48
|
+
if (saved?.status !== "consumed" || saved.memberId !== memberId || saved.action.split("__").at(-1) !== ctx.toolName || saved.expiresAt <= Date.now() || preview === void 0 || saved.source === void 0) throw new Error("Exact chat posting requires the original unexpired confirmed call.");
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
operation_id: chatPostOperationId(ctx.session.id, ctx.callId),
|
|
52
|
+
source: {
|
|
53
|
+
provider: "telegram",
|
|
54
|
+
chat_id: saved.source.chatId,
|
|
55
|
+
thread_id: saved.source.threadId
|
|
56
|
+
},
|
|
57
|
+
expected: {
|
|
58
|
+
chat_id: preview.chatId,
|
|
59
|
+
digest: chatPostDigest(preview)
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
} } } } : {},
|
|
34
63
|
auth: {
|
|
35
64
|
credentialOwner: "app",
|
|
36
65
|
getToken: async () => {
|
|
@@ -48,4 +77,18 @@ function ontologyConnection(input) {
|
|
|
48
77
|
})
|
|
49
78
|
});
|
|
50
79
|
}
|
|
51
|
-
|
|
80
|
+
async function ontologyConnectionForTurn(input) {
|
|
81
|
+
const memberId = memberIdOf(input.principal);
|
|
82
|
+
if (memberId === void 0) return null;
|
|
83
|
+
let chatPosting = false;
|
|
84
|
+
try {
|
|
85
|
+
chatPosting = await ontologyTools(input).supportsChatPosting(memberId);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn("Chat posting capability unavailable; keeping the compatible ontology connection.", error);
|
|
88
|
+
}
|
|
89
|
+
return ontologyConnection({
|
|
90
|
+
...input,
|
|
91
|
+
chatPosting
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export { ACT_AS_HEADER, MemberRequiredError, actAsHeaders, ontologyConnection, ontologyConnectionForTurn };
|
|
@@ -7,6 +7,7 @@ export declare function ontologyTools(options?: {
|
|
|
7
7
|
readonly token?: RuntimeToken;
|
|
8
8
|
readonly fetch?: typeof fetch;
|
|
9
9
|
}): {
|
|
10
|
+
supportsChatPosting(memberId: string): Promise<boolean>;
|
|
10
11
|
labels(memberId: string, action: string): Promise<ActionAnnotations>;
|
|
11
12
|
describe(memberId: string, action: string, input: Readonly<Record<string, unknown>>): Promise<import("./confirmation-description.js").ConfirmationDescription>;
|
|
12
13
|
};
|
|
@@ -6,6 +6,7 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { appOrigin } from "./environment.mjs";
|
|
8
8
|
import { describeConfirmation, object } from "./confirmation-description.mjs";
|
|
9
|
+
import { describeChatPost, isChatPost, supportsChatPost } from "./chat-post-confirmation.mjs";
|
|
9
10
|
import { redactDiagnostic } from "./diagnostics.mjs";
|
|
10
11
|
import { runtimeToken } from "./runtime-token.mjs";
|
|
11
12
|
function ontologyTools(options = {}) {
|
|
@@ -31,10 +32,12 @@ function ontologyTools(options = {}) {
|
|
|
31
32
|
signal: signal ?? AbortSignal.timeout(15e3),
|
|
32
33
|
redirect: "error"
|
|
33
34
|
});
|
|
34
|
-
if (!response.ok) throw new Error(`Cannot check action permission (HTTP ${response.status})
|
|
35
|
+
if (!response.ok) throw new Error(`Cannot check action permission (HTTP ${response.status}).`, { cause: await response.text() });
|
|
35
36
|
const body = object(await response.json());
|
|
36
37
|
if (body.error !== void 0) throw new Error("Ontology metadata unavailable.", { cause: body.error });
|
|
37
|
-
|
|
38
|
+
const result = object(body.result);
|
|
39
|
+
if (result.isError === true) throw new Error("The ontology preview read failed.", { cause: result });
|
|
40
|
+
return result;
|
|
38
41
|
} catch (error) {
|
|
39
42
|
throw new Error("The ontology metadata request failed.", { cause: redactDiagnostic(error, secrets) });
|
|
40
43
|
}
|
|
@@ -51,12 +54,34 @@ function ontologyTools(options = {}) {
|
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
return {
|
|
57
|
+
async supportsChatPosting(memberId) {
|
|
58
|
+
const result = await rpc(memberId, "tools/list", {});
|
|
59
|
+
return Array.isArray(result.tools) && result.tools.map(object).some(supportsChatPost);
|
|
60
|
+
},
|
|
54
61
|
async labels(memberId, action) {
|
|
55
62
|
return object((await toolFor(memberId, action)).tool.annotations);
|
|
56
63
|
},
|
|
57
64
|
async describe(memberId, action, input) {
|
|
58
65
|
const signal = AbortSignal.timeout(15e3);
|
|
59
66
|
const { tool, tools } = await toolFor(memberId, action, signal);
|
|
67
|
+
if (isChatPost(action)) {
|
|
68
|
+
if (!supportsChatPost(tool)) throw new Error("The ontology does not support exact chat confirmations.");
|
|
69
|
+
const read = async (name, args) => {
|
|
70
|
+
if (!tools.some((candidate) => candidate.name === name && object(candidate.annotations).readOnlyHint === true)) throw new Error("The chat preview reads are unavailable.");
|
|
71
|
+
const result = await rpc(memberId, "tools/call", {
|
|
72
|
+
name,
|
|
73
|
+
arguments: args
|
|
74
|
+
}, signal);
|
|
75
|
+
if (result.isError === true) throw new Error("The chat preview could not be resolved.");
|
|
76
|
+
return object(result.structuredContent);
|
|
77
|
+
};
|
|
78
|
+
const [caller, destinations] = await Promise.all([read("me", {}), read("chat_list", { chat: input.chat })]);
|
|
79
|
+
return describeChatPost({
|
|
80
|
+
values: input,
|
|
81
|
+
caller,
|
|
82
|
+
destinations
|
|
83
|
+
});
|
|
84
|
+
}
|
|
60
85
|
let entities = [];
|
|
61
86
|
try {
|
|
62
87
|
const catalog = await rpc(memberId, "resources/read", { uri: "ontology://catalog" }, signal);
|
|
@@ -36,7 +36,7 @@ export interface TelegramAdapterOptions {
|
|
|
36
36
|
readonly describeImage?: ImageDescriber;
|
|
37
37
|
readonly modelCapabilities?: CapabilityLookup;
|
|
38
38
|
readonly observed?: ObservedMessages;
|
|
39
|
-
/** Verifies callers of the
|
|
39
|
+
/** Verifies callers of the health route; tests pass one on a local key set. */
|
|
40
40
|
readonly verify?: RuntimeTokenVerifier;
|
|
41
41
|
/** Reads session events for routing, policy and interactions; tests pass a fake. */
|
|
42
42
|
readonly history?: HistoryReader;
|