@segmentstream/agent 0.5.4 → 0.6.3

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.
Files changed (29) hide show
  1. package/README.md +56 -18
  2. package/dist/extension/connections/ontology.mjs +2 -2
  3. package/dist/extension/instructions.md +10 -9
  4. package/dist/extension/lib/attachment-tools.mjs +1 -3
  5. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  6. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  7. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  8. package/dist/extension/lib/confirmations.d.ts +5 -0
  9. package/dist/extension/lib/confirmations.mjs +25 -5
  10. package/dist/extension/lib/conversations.d.ts +12 -1
  11. package/dist/extension/lib/conversations.mjs +4 -3
  12. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  13. package/dist/extension/lib/ontology-connection.mjs +46 -3
  14. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  15. package/dist/extension/lib/ontology-tools.mjs +27 -2
  16. package/dist/extension/lib/telegram/adapter.d.ts +1 -1
  17. package/dist/extension/lib/telegram/adapter.mjs +15 -26
  18. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  19. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  20. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  21. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  22. package/dist/extension/lib/telegram/customer-text.mjs +2 -1
  23. package/dist/extension/lib/telegram/health.mjs +1 -1
  24. package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
  25. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  26. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  27. package/package.json +1 -1
  28. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  29. 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
- - **Requests.** `POST /eve/v1/telegram/messages` takes a request from the ontology's
201
- `agent_chat.send_message`: a group or private chat, the request, the person to contact and the
202
- requester, with their roles (empty when an older ontology sends none) and `actAs` (whether the
203
- turn can act as them; without it, true for a member). The bearer must be a Vercel OIDC token of a production function of the same project,
204
- verified with the team's issuer and audience. The request reaches the chat's session through
205
- eve's proactive `receive` at `chatId::`, the address the chat's own messages continue, after the
206
- session policy (idle reset, daily compaction) runs on it, with an `<agent_request>` block naming the requester and the person. A member's request with `actAs` runs as that
207
- member, with their roles, which choose the model of a session the request starts; a service's,
208
- or an unpaired member's (`actAs: false`), runs without a member and has no ontology, its roles
209
- still choosing the model. It answers 202 with the session id, 503 outside
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,10 @@ 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
- Ontology `agent_chat.send_message` accepts optional `document_ids` in its REST/MCP input
316
- (`documentIds` in authored TypeScript), and forwards `documentIds` in the agent HTTP request.
317
- The `<agent_request>` block tells the agent to attach them with the question, one question per
318
- message. In groups follow `purpose` and `whenToUse`, and never send restricted documents.
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.
319
316
 
320
317
  ## Local development
321
318
 
@@ -362,8 +359,49 @@ attachment references. It reloads stored context for the same chat/topic each tu
362
359
  across cold starts and compaction. File bytes and tool output bodies are not archived.
363
360
 
364
361
  Retention defaults to 365 days per workspace. Platform REST reads, the workspace ontology's
365
- `agent_chat_history` and `agent_chat_search`, and the member REST facade enforce workspace/group
362
+ `chat_history` and `chat_search`, and the member REST facade enforce workspace/group
366
363
  permissions and DM participant/admin scope. Deploy the backend migration/API and upgrade the
367
364
  ontology SDK along with this extension. See the repository's
368
365
  [conversation storage guide](../../docs/agent-conversation-storage.md) for APIs, retention,
369
366
  eve's existing storage, and Telegram's delivery limits.
367
+
368
+ ### Exact chat posting protocol (prepared in agent SDK 0.6.0)
369
+
370
+ An authenticated `tools/list` advertises `_meta: { "segmentstream/chat-post": 1 }` on
371
+ `chat_post_message`. Without this marker,
372
+ ordinary tools use the compatible connection and no runtime arguments are added. Posting with
373
+ missing/incomplete preview metadata is denied. The versioned connection has its own instance key.
374
+
375
+ The runtime reads `me` for the server-derived `attribution`, and `chat_list({ chat })` for exactly
376
+ one eligible destination: `{ items: [{ id, title, provider, external_id }], next_cursor: null }`.
377
+ Provider is currently `telegram`. The final plain text is exactly `attribution + ": " + text`;
378
+ `reply_to` is optional. A preview digest is SHA-256 of the JSON array of chat ID, provider,
379
+ external ID, title, attribution, text, final text and nullable reply target, in that order.
380
+ The target/attribution are resolved again immediately before executing the frozen call.
381
+
382
+ Eve supplies the model-hidden `_segmentstream` argument on **all** calls of a negotiated connection.
383
+ It contains `version: 1`, `session_id` and `call_id`. A consumed, unexpired chat approval additionally
384
+ supplies `operation_id`, source provider/chat/topic coordinates and `expected: { chat_id, digest }`.
385
+ The operation ID is `hq:` plus SHA-256 of the JSON array `[session_id, call_id]`; it stays stable
386
+ on replay and changes for a revision. The model cannot provide an idempotency key or this envelope.
387
+ The ontology must authenticate runtime provenance, consume/remove the envelope before strict input
388
+ validation for ordinary tools, derive the posting key from the operation, and recompute attribution.
389
+ External OAuth/service callers still supply their own `idempotency_key` and cannot use this envelope.
390
+ These server changes belong to the later ontology stage; do not advertise the marker prematurely.
391
+
392
+ Source previews, accepted decisions and receipts use existing archive events with operation-linked
393
+ `toolCallId`/event IDs. A cancelled Edit carries its pending revision question in the authoritative
394
+ Eve resolution, so member/topic routing survives a cold start. No second provider sender lives here.
395
+
396
+ ## Release and migration
397
+
398
+ The tested release uses agent SDK 0.6.3, Eve 0.76.3 and ontology SDK 0.28.2.
399
+ Deploy the agent first, then backend archive migration 0033/API, then the ontology
400
+ with its declared Telegram provider mapping and explicit `agent_chat:post_message`
401
+ grant. Older ontologies keep ordinary tools working without the extra envelope.
402
+ An older archive returning only `{stored:true}` cannot authorize a send.
403
+
404
+ Rollback
405
+ disables posting while preserving archive claims and deduplication tombstones;
406
+ it never restores relay or retries uncertain deliveries.
407
+ 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 { ontologyConnection } from "../lib/ontology-connection.mjs";
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 ? ontologyConnection({ principal: context.session.auth.current }) : null } });
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
- - Use the ontology action `agent_chat.send_message` only to start a conversation in a different
32
- group or private chat; their replies come back into that chat's session, where you handle them.
33
- Inside a delivered `<agent_request>`, this turn already runs in the target chat: your reply here
34
- is the message delivered to that chat. Do not look for or call `agent_chat.send_message` to deliver
35
- it, even if the ontology catalog is empty or that action is unavailable. If the addressed person
36
- has a linked Telegram username, mention them with @username; otherwise address them by name
37
- without a tag. Never invent a username. The groups you are in are listed in `agent_chat`, with the
38
- `purpose` and `whenToUse` members set; when a member who may change it tells you what a group is
39
- for, record it with `agent_chat.update`.
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 attributes = input.principal?.attributes;
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 === "completed");
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 { actionApproval } from "./confirmations.mjs";
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
- export { ACT_AS_HEADER, MemberRequiredError, actAsHeaders, ontologyConnection };
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
- return object(body.result);
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 messages route; tests pass one on a local key set. */
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;
@@ -29,7 +29,6 @@ import { replyDelivery } from "./silent.mjs";
29
29
  import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
30
30
  import { handleInboundMessage } from "./inbound.mjs";
31
31
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
32
- import { messagesRoute } from "./requests.mjs";
33
32
  import { healthRoute } from "./health.mjs";
34
33
  import { telegramFailureEvents } from "./turn-errors.mjs";
35
34
  import { telegramWebhookVerifier } from "./webhook.mjs";
@@ -228,7 +227,7 @@ function segmentstreamTelegram(options) {
228
227
  },
229
228
  events: {
230
229
  ...telegramFailureEvents,
231
- ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision })),
230
+ ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision }), archive),
232
231
  "message.completed": replyDelivery(observed)
233
232
  }
234
233
  });
@@ -263,28 +262,17 @@ function segmentstreamTelegram(options) {
263
262
  }, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
264
263
  }]))
265
264
  } },
266
- routes: [
267
- ...channel.routes.map((route) => gatedRoute({
268
- route,
269
- options,
270
- environment,
271
- capture
272
- })),
273
- messagesRoute({
274
- channel,
275
- enabled: () => channelsEnabled(environment),
276
- configured: () => options.settings() !== null,
277
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
278
- sessionPolicy: policy,
279
- scope: () => options.sessions().scope
280
- }),
281
- healthRoute({
282
- chats,
283
- enabled: () => channelsEnabled(environment),
284
- configured: () => options.settings() !== null,
285
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
286
- })
287
- ]
265
+ routes: [...channel.routes.map((route) => gatedRoute({
266
+ route,
267
+ options,
268
+ environment,
269
+ capture
270
+ })), healthRoute({
271
+ chats,
272
+ enabled: () => channelsEnabled(environment),
273
+ configured: () => options.settings() !== null,
274
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
275
+ })]
288
276
  };
289
277
  }
290
278
  function sessionIdOf(context) {
@@ -305,9 +293,10 @@ function gatedRoute(input) {
305
293
  resolveSession: args.resolveSession,
306
294
  from: args.from
307
295
  };
308
- inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
296
+ inbound.respond = (address, requestId, decision, auth, text) => args.from(scoped(address)).respond([{
309
297
  requestId,
310
- optionId: decision
298
+ optionId: decision,
299
+ ...text === void 0 ? {} : { text }
311
300
  }], { auth });
312
301
  const tasks = [];
313
302
  const waitUntil = (task) => {
@@ -0,0 +1,15 @@
1
+ import type { ChatPostPreview } from "../chat-post-confirmation.js";
2
+ import type { ConfirmationText } from "./confirmation-text.js";
3
+ /** Only the frozen deterministic card may bypass the generic technical-prose filter. */
4
+ export declare function withChatPostCard<T>(text: string, run: () => T): T;
5
+ export declare function isExactChatPostCard(text: string): boolean;
6
+ export declare function chatPostCopy(locale: string): {
7
+ edit: string;
8
+ changes: string;
9
+ uncertain: string;
10
+ sent: string;
11
+ history: string;
12
+ expired: string;
13
+ };
14
+ export declare function chatPostCard(preview: ChatPostPreview, locale: string): ConfirmationText;
15
+ export declare function chatPostReceipt(output: unknown, locale: string): string | undefined;