@segmentstream/agent 0.5.3 → 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 (55) hide show
  1. package/README.md +140 -30
  2. package/dist/extension/config.d.ts +4 -0
  3. package/dist/extension/config.mjs +12 -3
  4. package/dist/extension/connections/ontology.mjs +2 -2
  5. package/dist/extension/instructions.md +10 -9
  6. package/dist/extension/lib/attachment-tools.mjs +1 -3
  7. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  8. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  9. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  10. package/dist/extension/lib/confirmations.d.ts +5 -0
  11. package/dist/extension/lib/confirmations.mjs +25 -5
  12. package/dist/extension/lib/conversation-runtime.mjs +18 -6
  13. package/dist/extension/lib/conversations.d.ts +12 -1
  14. package/dist/extension/lib/conversations.mjs +4 -3
  15. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  16. package/dist/extension/lib/ontology-connection.mjs +46 -3
  17. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  18. package/dist/extension/lib/ontology-tools.mjs +27 -2
  19. package/dist/extension/lib/telegram/adapter.d.ts +8 -2
  20. package/dist/extension/lib/telegram/adapter.mjs +52 -28
  21. package/dist/extension/lib/telegram/audience.d.ts +26 -0
  22. package/dist/extension/lib/telegram/audience.mjs +80 -0
  23. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  24. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  25. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  26. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  27. package/dist/extension/lib/telegram/content.d.ts +29 -0
  28. package/dist/extension/lib/telegram/content.mjs +161 -0
  29. package/dist/extension/lib/telegram/conversations.d.ts +2 -2
  30. package/dist/extension/lib/telegram/conversations.mjs +16 -8
  31. package/dist/extension/lib/telegram/customer-text.mjs +4 -3
  32. package/dist/extension/lib/telegram/files.d.ts +4 -0
  33. package/dist/extension/lib/telegram/files.mjs +33 -2
  34. package/dist/extension/lib/telegram/health.mjs +1 -1
  35. package/dist/extension/lib/telegram/images.d.ts +21 -0
  36. package/dist/extension/lib/telegram/images.mjs +47 -0
  37. package/dist/extension/lib/telegram/inbound-request.d.ts +6 -1
  38. package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
  39. package/dist/extension/lib/telegram/inbound.d.ts +10 -5
  40. package/dist/extension/lib/telegram/inbound.mjs +82 -22
  41. package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
  42. package/dist/extension/lib/telegram/media-policy.mjs +72 -0
  43. package/dist/extension/lib/telegram/observed.d.ts +8 -0
  44. package/dist/extension/lib/telegram/observed.mjs +10 -5
  45. package/dist/extension/lib/telegram/router.d.ts +10 -0
  46. package/dist/extension/lib/telegram/router.mjs +39 -32
  47. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  48. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  49. package/dist/extension/lib/telegram/silent.d.ts +2 -0
  50. package/dist/extension/lib/telegram/silent.mjs +8 -2
  51. package/dist/extension/lib/telegram/voice.d.ts +1 -0
  52. package/dist/extension/lib/telegram/voice.mjs +12 -6
  53. package/package.json +1 -1
  54. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  55. package/dist/extension/lib/telegram/requests.mjs +0 -186
package/README.md CHANGED
@@ -79,13 +79,14 @@ Both entry points export the same handle: `@segmentstream/agent` and
79
79
  }
80
80
  ```
81
81
 
82
- | Field | Default | Meaning |
83
- | ------------------------------ | --------------------- | ----------------------------------------------------------------------------------------------- |
84
- | `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
85
- | `channels.telegram.policy` | `"smart"` | Group messages that do not address the bot: `smart` asks the decision model, `mention` observes |
86
- | `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
87
- | `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
88
- | `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
82
+ | Field | Default | Meaning |
83
+ | ------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
84
+ | `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
85
+ | `channels.telegram.policy` | `"smart"` | Group messages that do not address the bot: `smart` asks the decision model, `mention` observes |
86
+ | `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
87
+ | `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
88
+ | `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
89
+ | `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
89
90
 
90
91
  An unknown field or an invalid value fails the mount with `SegmentStreamAgentConfigError`, naming
91
92
  each path. `manifestOf(config)` returns the normalized configuration, defaults applied, for the
@@ -97,7 +98,6 @@ platform's deployment manifest.
97
98
  | ------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98
99
  | `segmentstream__ontology` | `connections` | The app's `/mcp`, resolved per turn for the turn's member. No member, no connection. |
99
100
  | `segmentstream__telegram` | `channels` | `POST /eve/v1/telegram`: Telegram through the connection, with pairing, routing and voice. |
100
- | | | `POST /eve/v1/telegram/messages`: requests from the ontology's `agent_chat.send_message`. |
101
101
  | `segmentstream__attachments_list`, `_read`, `_promote`, `_discard` | `tools` | The member's temporary attachments in this chat: list, read, keep through an action, drop. |
102
102
  | `segmentstream__attachments_send` | `tools` | Sends readable stored ontology files into the current Telegram chat and topic. |
103
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 |
@@ -145,14 +145,28 @@ share a group chat's session never share a client.
145
145
  model, with the agent's description and the chat's recent observed messages. A message that is not
146
146
  answered joins a ring of the chat's last 30 observed messages. The next answered message carries
147
147
  the ring into its turn as context. A failed decision observes.
148
+ Replies to a known human or another bot, and explicit mentions of another recipient, are
149
+ observed before smart inference unless this bot is addressed. This also takes precedence over
150
+ a pending-question follow-up. Both mention entities and user-ID `text_mention` entities count;
151
+ quoted/code mentions do not. Smart routing receives the original body, reply author/text,
152
+ tagged users and whether this bot was tagged. Decision events retain a short routing audit note
153
+ alongside the reason code; this is an observable summary, not model chain-of-thought.
148
154
  - **Voice.** Voice messages, audio files and video notes are downloaded through the connection and
149
- transcribed with `models.stt` through AI Gateway. The transcript becomes the message, with a note
150
- such as `[voice message, 12 s]` in context. A file over Telegram's 20 MB download limit, or a
151
- failed transcription, reaches the agent as a note saying so.
155
+ transcribed with `models.stt` through AI Gateway before routing, including observations under
156
+ `mention`. The transcript becomes the message, with a note such as `[voice message, 12 s]` in
157
+ context. `stt: false` skips download and transcription. Transcription accepts at most 20 MiB
158
+ and ten minutes of declared duration, with a 60-second inference timeout. A skipped or failed
159
+ transcription reaches the agent as a note. Originals are not stored as ontology attachments.
160
+ Eve 0.76.3 only hydrates inbound images and PDFs; it does **not** deliver native audio bytes to
161
+ the chat model. Even an audio-capable chat model therefore uses the configured STT fallback,
162
+ with an explicit host limitation note. Native audio requires a compatible Eve host change.
152
163
  - **Attachments.** Photos and documents up to 20 MB stream through the connection proxy (its
153
164
  framed responses are not bound by the 2 MiB buffered limit) and are stored, before the turn,
154
165
  as the member's temporary attachments in the ontology's file storage
155
- (`POST /api/ontology/agent/attachments`), for this chat, for 24 hours. The turn's file parts
166
+ (`POST /api/ontology/agent/attachments`), for this chat, for 24 hours.
167
+ Observed group uploads are stored too, after participant registration, without starting a turn
168
+ or sending a typing indication. The next reply receives their references; attachment tools can
169
+ also list the stored files after a cold start. The turn's file parts
156
170
  point at the stored copies, which eve reads through the channel's `fetchFile` as the turn's
157
171
  session, which then owns them, and an `<attachments>` block names each attachment's id. A file
158
172
  that is too large or could not be stored becomes a note. The `segmentstream__attachments_*`
@@ -182,25 +196,80 @@ share a group chat's session never share a client.
182
196
  `migrate_from_chat_id` in the new one) reports the new id with `migratedFrom: <old id>`, starts
183
197
  no turn and drops the old chat from the memo. The `<chat>` block carries at most 1000
184
198
  characters of `whenToUse`.
185
- - **Requests.** `POST /eve/v1/telegram/messages` takes a request from the ontology's
186
- `agent_chat.send_message`: a group or private chat, the request, the person to contact and the
187
- requester, with their roles (empty when an older ontology sends none) and `actAs` (whether the
188
- 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,
189
- verified with the team's issuer and audience. The request reaches the chat's session through
190
- eve's proactive `receive` at `chatId::`, the address the chat's own messages continue, after the
191
- 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
192
- member, with their roles, which choose the model of a session the request starts; a service's,
193
- or an unpaired member's (`actAs: false`), runs without a member and has no ontology, its roles
194
- still choosing the model. It answers 202 with the session id, 503 outside
195
- production, 404 without `channels.telegram`, 401 for a missing or invalid token and 400 for a
196
- 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.
197
209
  - **Silence.** When the model answers a routed message with exactly `[SILENT]` (or `NO_REPLY`), in
198
- any case, nothing is sent to the chat. The turn stays in the session.
210
+ any case, nothing is sent to the chat. Empty and whitespace-only final text also send nothing.
211
+ The turn stays in the session. Archive decisions distinguish silence markers, empty text and
212
+ whitespace, retaining finish reason and character count without logging raw output. Model reply
213
+ decisions do not prove transport delivery; completed outbound events carry that evidence.
199
214
 
200
215
  The observed ring and chat-report caches live in process memory. On a cold start, the adapter
201
216
  restores recent conversation context from the platform archive and reports its chats afresh.
202
217
  The archive survives independently of eve session state and these caches.
203
218
 
219
+ Text and captions retain their Telegram entity annotations for addressing. Rich-message blocks
220
+ are projected to plain text before capture and dispatch, bounded to 16,384 UTF-16 code units,
221
+ 2048 traversal steps and 24 nested levels. The archive records content shape and truncation without retaining
222
+ raw webhook payloads. Edits remain separate audit events, but replace the earlier message in
223
+ observed/restored model context. An edit alone never starts an agent turn. Entity offsets cannot
224
+ reconstruct a missing body; unsupported empty content is recorded with shape diagnostics.
225
+
226
+ ### Image context and capabilities
227
+
228
+ Before handling media, the adapter reads the selected model's declared input modalities from
229
+ AI Gateway's public model metadata. Existing sessions use the model recorded in their latest
230
+ available `step.started` event, rather than reselecting by the current speaker's roles. A new
231
+ session uses `models.chat` with the normal role and preview rules. An inline model choice in
232
+ `agent.ts` cannot be inspected before its first session step; capabilities then stay unknown.
233
+
234
+ | Main chat input | `models.imageDescription` | Ingestion and later context |
235
+ | ------------------ | ------------------------- | ------------------------------------------------------------------------- |
236
+ | Images supported | omitted or `false` | Store the original and retain native file references; no description call |
237
+ | Any capability | a model id | Store the original and describe with that separate model before routing |
238
+ | Images unsupported | omitted | Describe with `google/gemini-2.5-flash-lite` before routing |
239
+ | Images unsupported | `false` | Store the original and record that analysis is disabled |
240
+ | Unknown | omitted or `false` | Store the original and record that analysis is unavailable |
241
+
242
+ Descriptions cover JPEG, PNG, WebP and GIF, at most one per Telegram message, 5 MiB of input,
243
+ 256 output tokens, 512 retained characters, and a 15-second inference timeout. Albums are
244
+ multiple Telegram messages and can incur multiple calls. These ingestion calls are separate
245
+ from the chat model's session spending limit. The descriptor treats image text as untrusted
246
+ evidence. Failure, unsupported format and limit notes retain the original attachment reference.
247
+ Metadata lookups have a five-second timeout, a five-minute success cache and a 30-second failure
248
+ cache, with at most 128 model entries. A lookup failure never asserts that the model saw pixels.
249
+
250
+ Eve's native image input limit is 3 MiB per image. On the next reply, the adapter also appends
251
+ eligible observed images as file parts, up to five images and 3 MiB total after accounting for
252
+ current images. It creates attachment URLs for the **current** requester, and storage checks that
253
+ member's access again. Expired or omitted images get notes. A text-only reply receives generated
254
+ descriptions and references, never hidden image bytes. Other formats remain attachment-tool inputs.
255
+
256
+ The process-local ring retains 30 observations per chat/topic, with separate budgets of 1000
257
+ UTF-16 code units of text and 2000 of media notes per observation, plus at most five image
258
+ references. The recent routing window also retains 30 entries. A process retains at most 500
259
+ chats, evicting the chat least recently written. Reading does not refresh its position. A reply
260
+ drains the observation backlog; an idle reset clears both windows; daily compaction leaves the
261
+ ring intact. Image bytes are not kept in this cache.
262
+
263
+ Archive restoration can recover raw text/captions after restarts and compaction: the webhook
264
+ warms an empty ring from up to 30 inbound events, and each turn refreshes a separate bounded
265
+ archive transcript. The latter reads at most 100 events, 4000 text characters and ten raw
266
+ attachment references per event, within 30,000 serialized characters overall. Generated
267
+ transcripts/descriptions and native-image ring references are currently **not** archived.
268
+ They can be lost on restart or eviction before the next reply; the originals of stored images
269
+ remain discoverable with attachment tools until expiry. Already dispatched native images follow
270
+ Eve's sandbox/session history and may be omitted by compaction. Durable enriched media context
271
+ and native audio are follow-up work, not guarantees of this adapter.
272
+
204
273
  The webhook subscribes to `message`, `edited_message`, `callback_query`, `my_chat_member`
205
274
  and `chat_member`. A bot join reports the group immediately, including when the inviter is
206
275
  unpaired; membership updates do not start turns. Report tasks participate in the wrapper's
@@ -240,10 +309,10 @@ JSON envelope and limits the decoded body to **2 MiB including multipart overhea
240
309
  not stream uploads. Files whose multipart body exceeds this limit return `too_large`; the
241
310
  larger Telegram limits do not bypass it. Downloads are bounded even if storage understates size.
242
311
 
243
- Ontology `agent_chat.send_message` accepts optional `document_ids` in its REST/MCP input
244
- (`documentIds` in authored TypeScript), and forwards `documentIds` in the agent HTTP request.
245
- The `<agent_request>` block tells the agent to attach them with the question, one question per
246
- 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.
247
316
 
248
317
  ## Local development
249
318
 
@@ -290,8 +359,49 @@ attachment references. It reloads stored context for the same chat/topic each tu
290
359
  across cold starts and compaction. File bytes and tool output bodies are not archived.
291
360
 
292
361
  Retention defaults to 365 days per workspace. Platform REST reads, the workspace ontology's
293
- `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
294
363
  permissions and DM participant/admin scope. Deploy the backend migration/API and upgrade the
295
364
  ontology SDK along with this extension. See the repository's
296
365
  [conversation storage guide](../../docs/agent-conversation-storage.md) for APIs, retention,
297
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).
@@ -14,6 +14,7 @@ export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
14
14
  export type TelegramPolicy = (typeof TELEGRAM_POLICIES)[number];
15
15
  /** The speech-to-text model voice messages are transcribed with, in the AI Gateway catalog. */
16
16
  export declare const DEFAULT_STT_MODEL = "spacexai/grok-stt";
17
+ export declare const DEFAULT_IMAGE_DESCRIPTION_MODEL = "google/gemini-2.5-flash-lite";
17
18
  /** The decision model the inbound router asks; eve's `decide` default. */
18
19
  export declare const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";
19
20
  /**
@@ -42,6 +43,8 @@ export interface SegmentStreamAgentConfigInput {
42
43
  };
43
44
  /** A Gateway transcription model, or `false` to leave voice messages untranscribed. */
44
45
  readonly stt?: string | boolean;
46
+ /** Separate vision model, or false to disable fallback descriptions; omitted prefers native input. */
47
+ readonly imageDescription?: string | boolean;
45
48
  /** The Gateway decision model of the inbound router. */
46
49
  readonly decision?: string;
47
50
  };
@@ -102,6 +105,7 @@ export interface SegmentStreamAgentConfig {
102
105
  /** `null` when `agent.ts` passes the choice to `segmentstream.model()` itself. */
103
106
  readonly chat: ChatModelChoice | null;
104
107
  readonly stt: string | false;
108
+ readonly imageDescription: string | false | null;
105
109
  readonly decision: string;
106
110
  };
107
111
  readonly ontology: {
@@ -7,6 +7,7 @@ __eveCreateRequire(import.meta.url);
7
7
  const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
8
8
  const TELEGRAM_POLICIES = ["mention", "smart"];
9
9
  const DEFAULT_STT_MODEL = "spacexai/grok-stt";
10
+ const DEFAULT_IMAGE_DESCRIPTION_MODEL = "google/gemini-2.5-flash-lite";
10
11
  const DEFAULT_DECISION_MODEL = "typesafe-ai/jev";
11
12
  const SESSION_SCOPES = ["chat", "thread"];
12
13
  var SegmentStreamAgentConfigError = class extends Error {
@@ -202,7 +203,8 @@ function validateConfig(input) {
202
203
  allowed: [
203
204
  "chat",
204
205
  "stt",
205
- "decision"
206
+ "decision",
207
+ "imageDescription"
206
208
  ],
207
209
  issues
208
210
  });
@@ -226,6 +228,12 @@ function validateConfig(input) {
226
228
  path: ["models", "decision"],
227
229
  issues
228
230
  });
231
+ const imageDescription = models?.imageDescription === void 0 ? null : models.imageDescription === false ? false : modelId({
232
+ value: models.imageDescription,
233
+ fallback: DEFAULT_IMAGE_DESCRIPTION_MODEL,
234
+ path: ["models", "imageDescription"],
235
+ issues
236
+ });
229
237
  const connection = ontology?.connection ?? true;
230
238
  const sessions = sessionSettings(root?.sessions, issues);
231
239
  if (typeof connection !== "boolean") issues.push({
@@ -238,7 +246,8 @@ function validateConfig(input) {
238
246
  models: {
239
247
  chat,
240
248
  stt,
241
- decision
249
+ decision,
250
+ imageDescription
242
251
  },
243
252
  ontology: { connection: connection === true },
244
253
  sessions
@@ -255,4 +264,4 @@ function manifestOf(config) {
255
264
  if (validation.issues !== void 0) throw new SegmentStreamAgentConfigError(validation.issues);
256
265
  return validation.value;
257
266
  }
258
- export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SESSION_SCOPES, SegmentStreamAgentConfigError, TELEGRAM_POLICIES, configSchema, manifestOf, validateConfig };
267
+ export { DEFAULT_DECISION_MODEL, DEFAULT_IMAGE_DESCRIPTION_MODEL, DEFAULT_STT_MODEL, SESSION_SCOPES, SegmentStreamAgentConfigError, TELEGRAM_POLICIES, configSchema, manifestOf, validateConfig };
@@ -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" };
@@ -7,21 +7,25 @@ __eveCreateRequire(import.meta.url);
7
7
  import { memberIdOf } from "./telegram/principal.mjs";
8
8
  import { conversationEvent, conversations, summarizeArguments } from "./conversations.mjs";
9
9
  import { parseAddress } from "./telegram/sessions.mjs";
10
- import { isSilentReply } from "./telegram/silent.mjs";
10
+ import { completionKind } from "./telegram/silent.mjs";
11
11
  function conversationCoordinates(context) {
12
12
  const principal = context.session.auth.current;
13
13
  const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
14
14
  const address = context.channel.continuationToken;
15
15
  const attributes = principal?.attributes;
16
- const chatId = telegram && address ? parseAddress(address).chatId : typeof attributes?.chatId === "string" ? attributes.chatId : address;
16
+ const fields = address?.split(":");
17
+ const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
18
+ const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
19
+ const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
20
+ const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
17
21
  if (!chatId) return;
18
22
  const memberId = memberIdOf(principal);
19
23
  const chatType = attributes?.chatType;
20
24
  return {
21
25
  provider: telegram ? "telegram" : context.channel.kind ?? "eve",
22
26
  chatId,
23
- chatType: chatType === "group" || chatType === "supergroup" ? "group" : chatType === "channel" ? "channel" : "private",
24
- threadId: telegram && address ? parseAddress(address).messageThreadId || null : null,
27
+ chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
28
+ threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
25
29
  participantIds: memberId === void 0 ? [] : [memberId],
26
30
  senderId: principal?.subject ?? null
27
31
  };
@@ -70,13 +74,21 @@ function conversationRecorder(archive = conversations()) {
70
74
  status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
71
75
  }));
72
76
  else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
73
- const silent = !event.data.message || isSilentReply(event.data.message);
77
+ const kind = completionKind(event.data.message, event.data.finishReason);
78
+ const silent = kind !== "reply";
79
+ const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
74
80
  await archive.append(conversationEvent({
75
81
  ...base,
76
82
  eventId,
77
83
  kind: "decision",
78
84
  decision: silent ? "silent" : "reply",
79
- reason: silent ? "model_silent_or_empty_output" : "model_reply"
85
+ reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
86
+ text: note,
87
+ argsSummary: JSON.stringify({
88
+ outputKind: kind,
89
+ outputCharacters: event.data.message?.length ?? 0,
90
+ finishReason: event.data.finishReason
91
+ })
80
92
  }));
81
93
  if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
82
94
  ...base,
@@ -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;