@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.
- package/README.md +140 -30
- package/dist/extension/config.d.ts +4 -0
- package/dist/extension/config.mjs +12 -3
- 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/conversation-runtime.mjs +18 -6
- 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 +8 -2
- package/dist/extension/lib/telegram/adapter.mjs +52 -28
- package/dist/extension/lib/telegram/audience.d.ts +26 -0
- package/dist/extension/lib/telegram/audience.mjs +80 -0
- 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/content.d.ts +29 -0
- package/dist/extension/lib/telegram/content.mjs +161 -0
- package/dist/extension/lib/telegram/conversations.d.ts +2 -2
- package/dist/extension/lib/telegram/conversations.mjs +16 -8
- package/dist/extension/lib/telegram/customer-text.mjs +4 -3
- package/dist/extension/lib/telegram/files.d.ts +4 -0
- package/dist/extension/lib/telegram/files.mjs +33 -2
- package/dist/extension/lib/telegram/health.mjs +1 -1
- package/dist/extension/lib/telegram/images.d.ts +21 -0
- package/dist/extension/lib/telegram/images.mjs +47 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +6 -1
- package/dist/extension/lib/telegram/inbound-request.mjs +11 -1
- package/dist/extension/lib/telegram/inbound.d.ts +10 -5
- package/dist/extension/lib/telegram/inbound.mjs +82 -22
- package/dist/extension/lib/telegram/media-policy.d.ts +15 -0
- package/dist/extension/lib/telegram/media-policy.mjs +72 -0
- package/dist/extension/lib/telegram/observed.d.ts +8 -0
- package/dist/extension/lib/telegram/observed.mjs +10 -5
- package/dist/extension/lib/telegram/router.d.ts +10 -0
- package/dist/extension/lib/telegram/router.mjs +39 -32
- 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/silent.d.ts +2 -0
- package/dist/extension/lib/telegram/silent.mjs +8 -2
- package/dist/extension/lib/telegram/voice.d.ts +1 -0
- package/dist/extension/lib/telegram/voice.mjs +12 -6
- package/package.json +1 -1
- package/dist/extension/lib/telegram/requests.d.ts +0 -80
- 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.
|
|
88
|
-
| `
|
|
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
|
|
150
|
-
such as `[voice message, 12 s]` in
|
|
151
|
-
|
|
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.
|
|
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
|
-
- **
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
`
|
|
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 {
|
|
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" };
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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;
|