@segmentstream/agent 0.8.1 → 0.8.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 +52 -0
- package/dist/_chunks/build-metadata-BLyaHLJW.mjs +16 -0
- package/dist/_chunks/{inbound-request-BwBvXk0A.mjs → inbound-request-CYSoY6pu.mjs} +35 -5
- package/dist/extension/config.d.ts +3 -0
- package/dist/extension/config.mjs +17 -2
- package/dist/extension/lib/attachment-tools.d.ts +5 -4
- package/dist/extension/lib/attachment-tools.mjs +3 -3
- package/dist/extension/lib/build-metadata.d.ts +8 -0
- package/dist/extension/lib/build-metadata.mjs +8 -0
- package/dist/extension/lib/confirmations.mjs +1 -1
- package/dist/extension/lib/deferred-confirmations.mjs +2 -2
- package/dist/extension/lib/ontology-tools.d.ts +9 -2
- package/dist/extension/lib/ontology-tools.mjs +48 -7
- package/dist/extension/lib/questions.d.ts +2 -0
- package/dist/extension/lib/stored-files.d.ts +17 -0
- package/dist/extension/lib/stored-files.mjs +70 -0
- package/dist/extension/lib/telegram/adapter.mjs +58 -4
- package/dist/extension/lib/telegram/attachment-description.mjs +1 -1
- package/dist/extension/lib/telegram/attachments-send.mjs +3 -3
- package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
- package/dist/extension/lib/telegram/confirmation-text.mjs +1 -1
- package/dist/extension/lib/telegram/confirmations.mjs +2 -2
- package/dist/extension/lib/telegram/content.d.ts +1 -1
- package/dist/extension/lib/telegram/conversations.mjs +1 -1
- package/dist/extension/lib/telegram/customer-text.d.ts +1 -1
- package/dist/extension/lib/telegram/customer-text.mjs +1 -1
- package/dist/extension/lib/telegram/deferred-confirmations.mjs +2 -2
- package/dist/extension/lib/telegram/files.mjs +1 -1
- package/dist/extension/lib/telegram/health.mjs +3 -1
- package/dist/extension/lib/telegram/images.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +11 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
- package/dist/extension/lib/telegram/inbound.d.ts +2 -0
- package/dist/extension/lib/telegram/inbound.mjs +147 -5
- package/dist/extension/lib/telegram/lifecycle.d.ts +46 -0
- package/dist/extension/lib/telegram/lifecycle.mjs +163 -0
- package/dist/extension/lib/telegram/media-policy.mjs +1 -1
- package/dist/extension/lib/telegram/silent.mjs +1 -1
- package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
- package/dist/extension/lib/telegram/voice.mjs +1 -1
- package/dist/extension/tools/attachments_discard.d.ts +4 -4
- package/dist/extension/tools/attachments_list.d.ts +4 -4
- package/dist/extension/tools/attachments_promote.d.ts +4 -4
- package/dist/extension/tools/attachments_promote.mjs +12 -7
- package/dist/extension/tools/attachments_read.mjs +17 -4
- package/dist/extension/tools/attachments_table.d.ts +4 -4
- package/dist/extension/tools/questions_ask.d.ts +3 -1
- package/dist/extension/tools/questions_ask.mjs +2 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -540,3 +540,55 @@ Rollback
|
|
|
540
540
|
disables posting while preserving archive claims and deduplication tombstones;
|
|
541
541
|
it never restores relay or retries uncertain deliveries.
|
|
542
542
|
See [migration and verification](../../docs/agent-chat-delivery-migration.md).
|
|
543
|
+
|
|
544
|
+
### Opt-in attachment lifecycle
|
|
545
|
+
|
|
546
|
+
`channels.telegram.attachmentLifecycle: { entity, action }` selects a permitted,
|
|
547
|
+
routine ontology action. There is no hook or collection delay by default. The
|
|
548
|
+
workspace action owns grouping, collection deadlines, language, retention and
|
|
549
|
+
completion; the SDK supplies no business policy or acknowledgement copy.
|
|
550
|
+
|
|
551
|
+
The action receives `phase` (`admitted`, `stored`, `retained`, `ready`, `completed`), a `source`
|
|
552
|
+
object (`chat`, `topic`, `message`, `sender`, `locale`) from the authenticated
|
|
553
|
+
Telegram request, and JSON `data`. Admission precedes downloads and history.
|
|
554
|
+
`data.attachments` records every expected Telegram file ID and filename; stored
|
|
555
|
+
and retained events record every permanent result or actionable error separately.
|
|
556
|
+
Writes execute as the current member; original source provenance grants no access.
|
|
557
|
+
|
|
558
|
+
Return JSON `{key, reply_to, acknowledgement, ready_at, retain, context}` as
|
|
559
|
+
needed: key identifies durable workspace state, reply_to fixes its source anchor,
|
|
560
|
+
ready_at is an epoch-millisecond collection deadline, and context is the current
|
|
561
|
+
durable inventory for the turn. Acknowledgement copy is workspace supplied;
|
|
562
|
+
claim it durably before returning it so webhook retries do not repeat it.
|
|
563
|
+
`retain: {entity, action, input}` requests immediate permanent storage of the
|
|
564
|
+
current file. Both the hook and retention target must resolve to permitted routine
|
|
565
|
+
actions. Record incomplete sources at admission and require all expected files
|
|
566
|
+
before business completion. Persist bill/result receipts and resume by business
|
|
567
|
+
identity after later uploads; a collection timer alone is insufficient.
|
|
568
|
+
|
|
569
|
+
The adapter retains the queued-turn policy, checks durable inbox acknowledgement,
|
|
570
|
+
and anchors delivery to the immutable source message/topic. An intake webhook
|
|
571
|
+
uses its host `waitUntil` lifetime to follow the durable session stream and refresh
|
|
572
|
+
Telegram typing every four seconds; completion, human input, cancellation,
|
|
573
|
+
failure, abort and the ten-minute observation bound close the reader and timer.
|
|
574
|
+
Safe phase telemetry distinguishes admission, storage, queue acceptance and the
|
|
575
|
+
owning turn without recording file contents or grants.
|
|
576
|
+
|
|
577
|
+
Before a final reply, an active lifecycle receives `completed`. Return
|
|
578
|
+
`{complete, message}` after checking durable results. The adapter uses this
|
|
579
|
+
workspace-owned message for both complete and incomplete outcomes; a failed
|
|
580
|
+
completion check cannot send an unchecked model success claim. Questions retain
|
|
581
|
+
the owning task and reply anchor across continuation turns.
|
|
582
|
+
|
|
583
|
+
Use `attachments_read` with `entity`, record `id`, and optional blob `property`
|
|
584
|
+
to resume from a permanent file after temporary attachment expiry. The SDK
|
|
585
|
+
obtains a fresh current-member `files_download` grant, streams only that grant's
|
|
586
|
+
headers, bounds the transfer and verifies immutable size/SHA before parsing.
|
|
587
|
+
The app bearer token and download grant never reach model output or diagnostics.
|
|
588
|
+
|
|
589
|
+
Returning a lifecycle key explicitly admits that source as a task, bypassing the
|
|
590
|
+
ordinary routing classifier. Omit the key to retain ordinary routing. Collection
|
|
591
|
+
honours the returned deadline within a 120-second processing budget; expiry fails
|
|
592
|
+
closed without queueing an incomplete turn. Authenticated runtime health includes
|
|
593
|
+
the built SDK version and deployment identity. Phase telemetry records webhook
|
|
594
|
+
receipt, lifecycle/queue admission, model/tool events and typing API acceptance.
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
var version = "0.8.3";
|
|
8
|
+
function agentBuild(environment = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
agent: version,
|
|
11
|
+
deploymentId: environment.VERCEL_DEPLOYMENT_ID ?? null,
|
|
12
|
+
sourceRevision: environment.VERCEL_GIT_COMMIT_SHA ?? null,
|
|
13
|
+
workspaceId: environment.SEGMENTSTREAM_WORKSPACE_ID ?? null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export { agentBuild as t };
|
|
@@ -5,19 +5,21 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { isTechnicalText } from "../extension/lib/customer-text-safety.mjs";
|
|
8
|
+
import { redactDiagnostic } from "../extension/lib/diagnostics.mjs";
|
|
8
9
|
import { isConfirmationText } from "../extension/lib/telegram/confirmation-text.mjs";
|
|
9
10
|
import { chatPostCardLinks, isExactChatPostCard } from "../extension/lib/telegram/chat-post-card.mjs";
|
|
10
|
-
import { redactDiagnostic } from "../extension/lib/diagnostics.mjs";
|
|
11
11
|
import { plainTelegramText, splitTelegramText, telegramMarkdown } from "../extension/lib/telegram/format.mjs";
|
|
12
12
|
import { inTelegramSendQueue } from "../extension/lib/telegram/send-queue.mjs";
|
|
13
13
|
import { telegramPreviewOptions, telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
|
|
14
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
14
15
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
|
-
import { randomBytes } from "node:crypto";
|
|
16
16
|
const deliveryLocale = new AsyncLocalStorage();
|
|
17
|
-
function withDeliveryLocale(locale, run, threadId) {
|
|
17
|
+
function withDeliveryLocale(locale, run, threadId, replyTo, chatId) {
|
|
18
18
|
return deliveryLocale.run({
|
|
19
19
|
locale,
|
|
20
|
-
threadId
|
|
20
|
+
threadId,
|
|
21
|
+
replyTo,
|
|
22
|
+
chatId
|
|
21
23
|
}, run);
|
|
22
24
|
}
|
|
23
25
|
function currentLocale() {
|
|
@@ -149,6 +151,12 @@ function safeTelegramFetch(fetchImpl, scope = "telegram", previews = {}) {
|
|
|
149
151
|
...item,
|
|
150
152
|
...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
|
|
151
153
|
}));
|
|
154
|
+
const replyTo = deliveryLocale.getStore()?.replyTo ?? currentInboundRequest()?.replyTo;
|
|
155
|
+
const replyChat = deliveryLocale.getStore()?.chatId ?? currentInboundRequest()?.chatId;
|
|
156
|
+
if (replyTo !== void 0 && replyChat === String(body.chat_id) && /\/send(?:Message|Photo|Document|Video|Audio|Voice|MediaGroup)$/.test(url) && body.reply_parameters === void 0 && body.reply_to_message_id === void 0) body.reply_parameters = {
|
|
157
|
+
message_id: Number(replyTo),
|
|
158
|
+
allow_sending_without_reply: true
|
|
159
|
+
};
|
|
152
160
|
const markup = body.reply_markup;
|
|
153
161
|
if (Array.isArray(markup?.inline_keyboard)) {
|
|
154
162
|
if ((textBlocked || typeof body.text === "string" && body.text.length > 4096) && markup.inline_keyboard.some((row) => row.some((button) => typeof button.callback_data === "string" && button.callback_data.startsWith("ss-confirm:")))) throw new Error("Approval buttons require the complete, safe confirmation text.");
|
|
@@ -224,7 +232,16 @@ function safeTelegramFetch(fetchImpl, scope = "telegram", previews = {}) {
|
|
|
224
232
|
var InboundRequest = class {
|
|
225
233
|
agentDescription;
|
|
226
234
|
agentName;
|
|
235
|
+
traceId = randomUUID();
|
|
227
236
|
edited = false;
|
|
237
|
+
triggeringMessageId;
|
|
238
|
+
chatId;
|
|
239
|
+
replyTo;
|
|
240
|
+
stopTyping;
|
|
241
|
+
lifecycle;
|
|
242
|
+
lifecycleKey;
|
|
243
|
+
lifecycleContext;
|
|
244
|
+
monitor;
|
|
228
245
|
previousText;
|
|
229
246
|
attachmentsChanged = false;
|
|
230
247
|
previousContent;
|
|
@@ -239,6 +256,7 @@ var InboundRequest = class {
|
|
|
239
256
|
files;
|
|
240
257
|
extraFiles;
|
|
241
258
|
sessions;
|
|
259
|
+
backgroundUntil;
|
|
242
260
|
waitUntil;
|
|
243
261
|
address;
|
|
244
262
|
confirmationEvent;
|
|
@@ -296,7 +314,9 @@ function withReplacedText(source, request) {
|
|
|
296
314
|
return {
|
|
297
315
|
...source,
|
|
298
316
|
async send(message, options) {
|
|
299
|
-
const
|
|
317
|
+
const marker = request.lifecycleKey === void 0 || request.replyTo === void 0 ? void 0 : `[source-message:${request.replyTo};trigger:${request.triggeringMessageId ?? request.replyTo}]`;
|
|
318
|
+
const replacement = marker === void 0 ? request.text : `${marker}\n${request.text ?? (typeof message === "string" ? message : "")}`;
|
|
319
|
+
const text = replacement === void 0 ? message : replaceText(message, replacement);
|
|
300
320
|
const files = request.files === void 0 ? text : replaceFiles(text, request.files);
|
|
301
321
|
const content = request.extraFiles === void 0 || request.extraFiles.length === 0 ? files : [...typeof files === "string" ? [{
|
|
302
322
|
type: "text",
|
|
@@ -325,6 +345,16 @@ function withReplacedText(source, request) {
|
|
|
325
345
|
} catch (error) {
|
|
326
346
|
reportTelegramError("turn_admission_recording_failed", error);
|
|
327
347
|
}
|
|
348
|
+
if (request.lifecycleKey !== void 0) console.info("[segmentstream] telegram.phase", {
|
|
349
|
+
phase: "queue_accepted",
|
|
350
|
+
traceId: request.traceId,
|
|
351
|
+
at: new Date().toISOString(),
|
|
352
|
+
chatId: request.chatId,
|
|
353
|
+
messageId: request.replyTo,
|
|
354
|
+
intakeId: request.lifecycleKey,
|
|
355
|
+
sessionId: result.id
|
|
356
|
+
});
|
|
357
|
+
if (marker !== void 0) request.monitor?.(result, marker);
|
|
328
358
|
return result;
|
|
329
359
|
}
|
|
330
360
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AttachmentLifecycle } from "./lib/telegram/lifecycle.js";
|
|
1
2
|
import { type TelegramLinkPreviews } from "@segmentstream-cloud/ontology/telegram";
|
|
2
3
|
export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
|
|
3
4
|
/**
|
|
@@ -62,6 +63,7 @@ export interface SegmentStreamAgentConfigInput {
|
|
|
62
63
|
readonly linkPreviews?: string;
|
|
63
64
|
/** Additional exact HTTP(S) origins belonging to this workspace. */
|
|
64
65
|
readonly additionalOwnOrigins?: readonly string[];
|
|
66
|
+
readonly attachmentLifecycle?: AttachmentLifecycle;
|
|
65
67
|
};
|
|
66
68
|
};
|
|
67
69
|
readonly models?: {
|
|
@@ -114,6 +116,7 @@ export interface TelegramSettings {
|
|
|
114
116
|
readonly policy: TelegramPolicy;
|
|
115
117
|
readonly linkPreviews?: TelegramLinkPreviews;
|
|
116
118
|
readonly additionalOwnOrigins?: readonly string[];
|
|
119
|
+
readonly attachmentLifecycle?: AttachmentLifecycle;
|
|
117
120
|
}
|
|
118
121
|
/** The chat models `segmentstream.model()` chooses between, as AI Gateway model ids. */
|
|
119
122
|
export interface ChatModelChoice {
|
|
@@ -66,7 +66,8 @@ function telegramSettings(value, issues) {
|
|
|
66
66
|
"connection",
|
|
67
67
|
"policy",
|
|
68
68
|
"linkPreviews",
|
|
69
|
-
"additionalOwnOrigins"
|
|
69
|
+
"additionalOwnOrigins",
|
|
70
|
+
"attachmentLifecycle"
|
|
70
71
|
],
|
|
71
72
|
issues
|
|
72
73
|
});
|
|
@@ -91,11 +92,25 @@ function telegramSettings(value, issues) {
|
|
|
91
92
|
message: "must be an array of HTTP(S) origins without paths, credentials, queries or fragments",
|
|
92
93
|
path: [...path, "additionalOwnOrigins"]
|
|
93
94
|
});
|
|
95
|
+
const lifecycle = section({
|
|
96
|
+
value: telegram.attachmentLifecycle,
|
|
97
|
+
path: [...path, "attachmentLifecycle"],
|
|
98
|
+
allowed: ["entity", "action"],
|
|
99
|
+
issues
|
|
100
|
+
});
|
|
101
|
+
if (lifecycle !== void 0 && ["entity", "action"].some((key) => typeof lifecycle[key] !== "string" || !/^(?:[a-z][a-zA-Z0-9]*|[a-z][a-z0-9]*(?:_[a-z0-9]+)*)$/.test(String(lifecycle[key])))) issues.push({
|
|
102
|
+
message: "must name a valid ontology entity and action",
|
|
103
|
+
path: [...path, "attachmentLifecycle"]
|
|
104
|
+
});
|
|
94
105
|
return typeof connection === "string" && isTelegramPolicy(policy) ? {
|
|
95
106
|
connection,
|
|
96
107
|
policy,
|
|
97
108
|
linkPreviews,
|
|
98
|
-
additionalOwnOrigins
|
|
109
|
+
additionalOwnOrigins,
|
|
110
|
+
...lifecycle === void 0 ? {} : { attachmentLifecycle: {
|
|
111
|
+
entity: String(lifecycle.entity),
|
|
112
|
+
action: String(lifecycle.action)
|
|
113
|
+
} }
|
|
99
114
|
} : null;
|
|
100
115
|
}
|
|
101
116
|
function modelId(input) {
|
|
@@ -25,15 +25,16 @@ export declare function attachmentOwner(input: {
|
|
|
25
25
|
/** The process's attachment client, on the deployment's own origin and OIDC token. */
|
|
26
26
|
export declare function toolAttachments(environment?: Environment, token?: RuntimeToken): AgentAttachments;
|
|
27
27
|
/** An attachment as the tools answer it. */
|
|
28
|
-
export
|
|
28
|
+
export type ReadableFile = Pick<Attachment, "id" | "filename" | "contentType" | "size" | "sha256"> & Partial<Pick<Attachment, "state" | "expiresAt" | "promotion">>;
|
|
29
|
+
export declare function attachmentForModel(attachment: ReadableFile): {
|
|
29
30
|
promotion?: {} | undefined;
|
|
31
|
+
expires_at?: string | undefined;
|
|
32
|
+
state?: string | undefined;
|
|
30
33
|
id: string;
|
|
31
34
|
filename: string;
|
|
32
35
|
content_type: string;
|
|
33
36
|
size: number;
|
|
34
37
|
sha256: string;
|
|
35
|
-
state: string;
|
|
36
|
-
expires_at: string;
|
|
37
38
|
};
|
|
38
39
|
export interface ReadOptions {
|
|
39
40
|
readonly sheet?: string;
|
|
@@ -80,7 +81,7 @@ export declare function headerArgument(value: unknown): number | undefined;
|
|
|
80
81
|
/** Keep envelopes and structured query results inside the same response budget as file text. */
|
|
81
82
|
export declare function boundedResultPage<T>(items: readonly T[], offset: number, limit: number, envelope: unknown): T[];
|
|
82
83
|
/** Reads never discard the remainder. Parsers process the entire file before selecting a page. */
|
|
83
|
-
export declare function readOutput(attachment:
|
|
84
|
+
export declare function readOutput(attachment: ReadableFile, bytes: Uint8Array, options?: ReadOptions): Promise<ReadOutput>;
|
|
84
85
|
/** A required string argument. */
|
|
85
86
|
export declare function stringArgument(input: Readonly<Record<string, unknown>>, name: string): string;
|
|
86
87
|
/** An optional string argument. */
|
|
@@ -41,9 +41,9 @@ function attachmentForModel(attachment) {
|
|
|
41
41
|
content_type: attachment.contentType,
|
|
42
42
|
size: attachment.size,
|
|
43
43
|
sha256: attachment.sha256,
|
|
44
|
-
state: attachment.state,
|
|
45
|
-
expires_at: attachment.expiresAt,
|
|
46
|
-
...attachment.promotion
|
|
44
|
+
...attachment.state === void 0 ? {} : { state: attachment.state },
|
|
45
|
+
...attachment.expiresAt === void 0 ? {} : { expires_at: attachment.expiresAt },
|
|
46
|
+
...attachment.promotion == null ? {} : { promotion: attachment.promotion }
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
function pageInteger(value, fallback, maximum = Number.MAX_SAFE_INTEGER) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Environment } from "./environment.js";
|
|
2
|
+
/** Build identity only; credentials and provider payloads never enter runtime provenance. */
|
|
3
|
+
export declare function agentBuild(environment?: Environment): {
|
|
4
|
+
agent: string;
|
|
5
|
+
deploymentId: string | null;
|
|
6
|
+
sourceRevision: string | null;
|
|
7
|
+
workspaceId: string | null;
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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 { t as agentBuild } from "../../_chunks/build-metadata-BLyaHLJW.mjs";
|
|
8
|
+
export { agentBuild };
|
|
@@ -6,8 +6,8 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { memberIdOf } from "./telegram/principal.mjs";
|
|
8
8
|
import { confirmationCopy, renderConfirmation } from "./telegram/confirmation-text.mjs";
|
|
9
|
-
import { chatPostDigest, isChatPost } from "./chat-post-confirmation.mjs";
|
|
10
9
|
import { chatPostCard } from "./telegram/chat-post-card.mjs";
|
|
10
|
+
import { chatPostDigest, isChatPost } from "./chat-post-confirmation.mjs";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { defineState } from "eve/context";
|
|
13
13
|
const CONFIRMATION_TTL_MS = 6e5;
|
|
@@ -6,10 +6,10 @@ __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 { redactDiagnostic } from "./diagnostics.mjs";
|
|
9
10
|
import { humanText, object } from "./confirmation-description.mjs";
|
|
10
11
|
import { chatPostDigest, chatPostOperationId, isChatPost } from "./chat-post-confirmation.mjs";
|
|
11
12
|
import { actionApproval, inputHash } from "./confirmations.mjs";
|
|
12
|
-
import { redactDiagnostic } from "./diagnostics.mjs";
|
|
13
13
|
import { runtimeToken } from "./runtime-token.mjs";
|
|
14
14
|
import { ontologyTools } from "./ontology-tools.mjs";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
@@ -111,7 +111,7 @@ async function executeDeferredTool(memberId, action, input, ctx, options = {}) {
|
|
|
111
111
|
async function runDeferredAction(record, options) {
|
|
112
112
|
if (record.inputHash !== inputHash(record.data.input)) throw new Error("The confirmed input changed.");
|
|
113
113
|
const tools = ontologyTools(options);
|
|
114
|
-
await tools.labels(record.memberId, record.data.action);
|
|
114
|
+
await tools.labels(record.memberId, record.data.action, record.data.input);
|
|
115
115
|
const preview = record.data.confirmation.description?.chatPost;
|
|
116
116
|
if (isChatPost(record.data.action)) {
|
|
117
117
|
const input = Object.fromEntries(Object.entries(record.data.input).filter(([key]) => key !== "_confirmation"));
|
|
@@ -12,9 +12,16 @@ export declare function ontologyTools(options?: {
|
|
|
12
12
|
rpc: (memberId: string, method: string, params: Record<string, unknown>, signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
13
13
|
list(memberId: string): Promise<Record<string, unknown>[]>;
|
|
14
14
|
supportsChatPosting(memberId: string): Promise<boolean>;
|
|
15
|
-
labels(memberId: string, action: string): Promise<ActionAnnotations>;
|
|
15
|
+
labels(memberId: string, action: string, input?: Readonly<Record<string, unknown>>): Promise<ActionAnnotations>;
|
|
16
16
|
describe(memberId: string, action: string, input: Readonly<Record<string, unknown>>): Promise<import("./confirmation-description.js").ConfirmationDescription>;
|
|
17
17
|
};
|
|
18
|
-
export declare function ontologyToolLabels(options?: Parameters<typeof ontologyTools>[0]): (memberId: string, action: string) => Promise<ActionAnnotations>;
|
|
18
|
+
export declare function ontologyToolLabels(options?: Parameters<typeof ontologyTools>[0]): (memberId: string, action: string, input?: Readonly<Record<string, unknown>>) => Promise<ActionAnnotations>;
|
|
19
19
|
/** MCP normalizes an authored camelCase action to snake_case. */
|
|
20
20
|
export declare function actionToolName(entity: string, action: string): string;
|
|
21
|
+
/** One target contract for native and MCP attachment promotion; never trust supplied safety hints. */
|
|
22
|
+
export declare function promotionTarget(input: Readonly<Record<string, unknown>>): {
|
|
23
|
+
action: string;
|
|
24
|
+
input: {
|
|
25
|
+
id?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -6,9 +6,9 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { appOrigin } from "./environment.mjs";
|
|
8
8
|
import { identityProviderOf, memberIdOf } from "./telegram/principal.mjs";
|
|
9
|
+
import { redactDiagnostic } from "./diagnostics.mjs";
|
|
9
10
|
import { describeConfirmation, object } from "./confirmation-description.mjs";
|
|
10
11
|
import { describeChatPost, isChatPost, supportsChatPost } from "./chat-post-confirmation.mjs";
|
|
11
|
-
import { redactDiagnostic } from "./diagnostics.mjs";
|
|
12
12
|
import { runtimeToken } from "./runtime-token.mjs";
|
|
13
13
|
function ontologyTools(options = {}) {
|
|
14
14
|
async function rpc(memberId, method, params, signal) {
|
|
@@ -49,13 +49,38 @@ function ontologyTools(options = {}) {
|
|
|
49
49
|
const result = await rpc(memberId, "tools/list", {}, signal);
|
|
50
50
|
const tools = Array.isArray(result.tools) ? result.tools.map(object) : [];
|
|
51
51
|
const bareName = action.split("__").at(-1);
|
|
52
|
-
|
|
52
|
+
let tool = tools.find((candidate) => candidate.name === bareName);
|
|
53
|
+
if (tool === void 0 && tools.some((item) => item.name === "ontology_describe_tool")) {
|
|
54
|
+
const inspected = await rpc(memberId, "tools/call", {
|
|
55
|
+
name: "ontology_describe_tool",
|
|
56
|
+
arguments: { name: bareName }
|
|
57
|
+
}, signal);
|
|
58
|
+
const candidate = object(object(inspected.structuredContent).tool);
|
|
59
|
+
if (candidate.name === bareName) tool = candidate;
|
|
60
|
+
}
|
|
53
61
|
if (tool === void 0) throw new Error("Action not permitted.");
|
|
54
62
|
return {
|
|
55
63
|
tool,
|
|
56
64
|
tools
|
|
57
65
|
};
|
|
58
66
|
}
|
|
67
|
+
async function resolve(memberId, action, input, signal, depth = 0) {
|
|
68
|
+
if (depth > 3) throw new Error("Too many nested ontology tools.");
|
|
69
|
+
const found = await toolFor(memberId, action, signal);
|
|
70
|
+
if (depth > 0 && isChatPost(String(found.tool.name))) throw new Error("Exact chat posting must use the direct chat_post_message tool.");
|
|
71
|
+
if (found.tool.name === "ontology_call_tool") {
|
|
72
|
+
if (typeof input.name !== "string") throw new Error("A target tool is required.");
|
|
73
|
+
return resolve(memberId, input.name, object(input.arguments), signal, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
if (found.tool.name === "attachments_promote") {
|
|
76
|
+
const target = promotionTarget(input);
|
|
77
|
+
return resolve(memberId, target.action, target.input, signal, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...found,
|
|
81
|
+
input
|
|
82
|
+
};
|
|
83
|
+
}
|
|
59
84
|
return {
|
|
60
85
|
rpc,
|
|
61
86
|
async list(memberId) {
|
|
@@ -66,8 +91,8 @@ function ontologyTools(options = {}) {
|
|
|
66
91
|
const result = await rpc(memberId, "tools/list", {});
|
|
67
92
|
return Array.isArray(result.tools) && result.tools.map(object).some(supportsChatPost);
|
|
68
93
|
},
|
|
69
|
-
async labels(memberId, action) {
|
|
70
|
-
const { tool } = await
|
|
94
|
+
async labels(memberId, action, input = {}) {
|
|
95
|
+
const { tool } = await resolve(memberId, action, input);
|
|
71
96
|
return {
|
|
72
97
|
...object(tool.annotations),
|
|
73
98
|
confirmation: object(object(tool.inputSchema).properties)._confirmation !== void 0
|
|
@@ -75,7 +100,10 @@ function ontologyTools(options = {}) {
|
|
|
75
100
|
},
|
|
76
101
|
async describe(memberId, action, input) {
|
|
77
102
|
const signal = AbortSignal.timeout(15e3);
|
|
78
|
-
const
|
|
103
|
+
const resolved = await resolve(memberId, action, input, signal);
|
|
104
|
+
const { tool, tools } = resolved;
|
|
105
|
+
input = resolved.input;
|
|
106
|
+
action = String(tool.name);
|
|
79
107
|
if (isChatPost(action)) {
|
|
80
108
|
if (!supportsChatPost(tool)) throw new Error("The ontology does not support exact chat confirmations.");
|
|
81
109
|
const read = async (name, args) => {
|
|
@@ -125,8 +153,9 @@ function ontologyTools(options = {}) {
|
|
|
125
153
|
values: input,
|
|
126
154
|
async read(entityName, id) {
|
|
127
155
|
const name = actionToolName(entityName, "get");
|
|
128
|
-
if (!tools.some((candidate) => candidate.name === name && object(candidate.annotations).readOnlyHint === true)) return;
|
|
129
156
|
try {
|
|
157
|
+
const permitted = (await toolFor(memberId, name, signal)).tool;
|
|
158
|
+
if (object(permitted.annotations).readOnlyHint !== true) return;
|
|
130
159
|
const result = await rpc(memberId, "tools/call", {
|
|
131
160
|
name,
|
|
132
161
|
arguments: { id }
|
|
@@ -148,4 +177,16 @@ function actionToolName(entity, action) {
|
|
|
148
177
|
const snake = (name) => name.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
149
178
|
return `${snake(entity)}_${snake(action)}`;
|
|
150
179
|
}
|
|
151
|
-
|
|
180
|
+
function promotionTarget(input) {
|
|
181
|
+
const entity = input.entity;
|
|
182
|
+
const action = input.action;
|
|
183
|
+
if (typeof entity !== "string" || typeof action !== "string" || !/^[a-z][a-zA-Z0-9_]*$/.test(entity) || !/^[a-z][a-zA-Z0-9_]*$/.test(action)) throw new Error("A valid attachment target entity and action are required.");
|
|
184
|
+
return {
|
|
185
|
+
action: actionToolName(entity, action),
|
|
186
|
+
input: {
|
|
187
|
+
...object(input.input),
|
|
188
|
+
...typeof input.record_id === "string" ? { id: input.record_id } : {}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export { actionToolName, ontologyToolLabels, ontologyTools, promotionTarget };
|
|
@@ -6,6 +6,8 @@ export interface PendingQuestion {
|
|
|
6
6
|
readonly chatId: string;
|
|
7
7
|
readonly messageThreadId: string;
|
|
8
8
|
readonly address: string;
|
|
9
|
+
readonly replyToMessageId?: string;
|
|
10
|
+
readonly taskId?: string;
|
|
9
11
|
}
|
|
10
12
|
/** Intent survives turns and compaction in the session, independently of model history. */
|
|
11
13
|
export declare const pendingQuestions: import("eve/context").StateHandle<Record<string, PendingQuestion>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
|
+
import { ontologyTools } from "./ontology-tools.js";
|
|
3
|
+
/** A permanent blob is read through the same current-member grant used by the ontology UI. */
|
|
4
|
+
export declare function readStoredFile(input: {
|
|
5
|
+
readonly entity: string;
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly property?: string;
|
|
8
|
+
}, principal: SessionAuthContext | null, options?: Parameters<typeof ontologyTools>[0]): Promise<{
|
|
9
|
+
attachment: {
|
|
10
|
+
id: string;
|
|
11
|
+
filename: string;
|
|
12
|
+
contentType: string;
|
|
13
|
+
size: number;
|
|
14
|
+
sha256: string;
|
|
15
|
+
};
|
|
16
|
+
bytes: Buffer<ArrayBuffer>;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { memberIdOf } from "./telegram/principal.mjs";
|
|
8
|
+
import { redactDiagnostic } from "./diagnostics.mjs";
|
|
9
|
+
import { object } from "./confirmation-description.mjs";
|
|
10
|
+
import { ontologyTools } from "./ontology-tools.mjs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
async function readStoredFile(input, principal, options = {}) {
|
|
13
|
+
const memberId = memberIdOf(principal);
|
|
14
|
+
if (memberId === void 0) throw new Error("A current member is required to read a stored file.");
|
|
15
|
+
const raw = await ontologyTools({
|
|
16
|
+
...options,
|
|
17
|
+
principal
|
|
18
|
+
}).rpc(memberId, "tools/call", {
|
|
19
|
+
name: "files_download",
|
|
20
|
+
arguments: input
|
|
21
|
+
});
|
|
22
|
+
const result = object(raw.structuredContent), file = object(result.file), grant = object(result.download);
|
|
23
|
+
const headers = object(grant.headers);
|
|
24
|
+
const size = file.size;
|
|
25
|
+
const secrets = new Set([...typeof grant.url === "string" ? [grant.url] : [], ...Object.values(headers).filter((value) => typeof value === "string")]);
|
|
26
|
+
try {
|
|
27
|
+
if (file.id !== input.id || file.entity !== input.entity || file.property !== (input.property ?? "content") || grant.method !== "GET" || typeof file.filename !== "string" || typeof file.content_type !== "string" || typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(file.sha256) || typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > 26214400 || typeof grant.url !== "string" || new URL(grant.url).protocol !== "https:" || Object.values(headers).some((value) => typeof value !== "string")) throw new Error("The stored file grant or bounded metadata is invalid.", { cause: {
|
|
28
|
+
file,
|
|
29
|
+
download: grant
|
|
30
|
+
} });
|
|
31
|
+
const response = await (options.fetch ?? fetch)(grant.url, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)])),
|
|
34
|
+
redirect: "error",
|
|
35
|
+
signal: AbortSignal.timeout(12e4)
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok || response.body === null) throw new Error(`Stored file transfer refused (HTTP ${response.status}).`);
|
|
38
|
+
const reader = response.body.getReader(), chunks = [];
|
|
39
|
+
let received = 0;
|
|
40
|
+
try {
|
|
41
|
+
for (;;) {
|
|
42
|
+
const chunk = await reader.read();
|
|
43
|
+
if (chunk.done) break;
|
|
44
|
+
received += chunk.value.byteLength;
|
|
45
|
+
if (received > size) throw new Error("Stored file transfer exceeded its declared size.");
|
|
46
|
+
chunks.push(chunk.value);
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
await reader.cancel().catch((error) => {
|
|
50
|
+
console.warn("Stored file reader cleanup failed.", redactDiagnostic(error, secrets));
|
|
51
|
+
});
|
|
52
|
+
reader.releaseLock();
|
|
53
|
+
}
|
|
54
|
+
const bytes = Buffer.concat(chunks);
|
|
55
|
+
if (bytes.byteLength !== size || createHash("sha256").update(bytes).digest("hex") !== file.sha256) throw new Error("Stored file bytes differ from their immutable revision metadata.");
|
|
56
|
+
return {
|
|
57
|
+
attachment: {
|
|
58
|
+
id: input.id,
|
|
59
|
+
filename: file.filename,
|
|
60
|
+
contentType: file.content_type,
|
|
61
|
+
size,
|
|
62
|
+
sha256: file.sha256
|
|
63
|
+
},
|
|
64
|
+
bytes
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error("The permanent file could not be read.", { cause: redactDiagnostic(error, secrets) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export { readStoredFile };
|
|
@@ -8,11 +8,13 @@ import { manifestOf } from "../../config.mjs";
|
|
|
8
8
|
import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
|
|
9
9
|
import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
|
|
10
10
|
import { chooseModel } from "../../model.mjs";
|
|
11
|
+
import { t as agentBuild } from "../../../_chunks/build-metadata-BLyaHLJW.mjs";
|
|
11
12
|
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
13
|
+
import { telegramSendScope } from "./send-queue.mjs";
|
|
14
|
+
import { d as failureText, h as withDeliveryLocale, i as withReplacedText, m as safeTelegramFetch, n as currentInboundRequest, p as reportTelegramError, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
12
15
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
16
|
+
import { attachmentLifecycle } from "./lifecycle.mjs";
|
|
13
17
|
import { conversations } from "../conversations.mjs";
|
|
14
|
-
import { telegramSendScope } from "./send-queue.mjs";
|
|
15
|
-
import { d as failureText, h as withDeliveryLocale, i as withReplacedText, m as safeTelegramFetch, n as currentInboundRequest, p as reportTelegramError, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
|
|
16
18
|
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
17
19
|
import { observedMessages } from "./observed.mjs";
|
|
18
20
|
import { expireDeferredCards, handleDeferredCallback, isDeferredCallback } from "./deferred-confirmations.mjs";
|
|
@@ -164,6 +166,15 @@ function segmentstreamTelegram(options) {
|
|
|
164
166
|
request: currentInboundRequest(),
|
|
165
167
|
dependencies: {
|
|
166
168
|
policy: settings().policy,
|
|
169
|
+
...settings().attachmentLifecycle === void 0 ? {} : { lifecycle: (principal, memberId, message) => attachmentLifecycle({
|
|
170
|
+
configuration: settings().attachmentLifecycle,
|
|
171
|
+
principal,
|
|
172
|
+
memberId,
|
|
173
|
+
message,
|
|
174
|
+
environment,
|
|
175
|
+
token,
|
|
176
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
177
|
+
}) },
|
|
167
178
|
sessions: options.sessions,
|
|
168
179
|
admission: options.admission ?? (() => manifestOf({})),
|
|
169
180
|
sessionPolicy: policy,
|
|
@@ -243,6 +254,8 @@ function segmentstreamTelegram(options) {
|
|
|
243
254
|
const errorId = reportTelegramError("inbound_message_failed", error, { chatId: message.chat.id });
|
|
244
255
|
await context.telegram.post(failureText(errorId));
|
|
245
256
|
return null;
|
|
257
|
+
} finally {
|
|
258
|
+
inbound?.stopTyping?.();
|
|
246
259
|
}
|
|
247
260
|
},
|
|
248
261
|
onCallbackQuery: async (context, query) => {
|
|
@@ -284,7 +297,41 @@ function segmentstreamTelegram(options) {
|
|
|
284
297
|
token,
|
|
285
298
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
286
299
|
}),
|
|
287
|
-
"message.completed"
|
|
300
|
+
async "message.completed"(event, channel, context) {
|
|
301
|
+
const principal = context.session.auth.current;
|
|
302
|
+
const taskId = principal?.attributes.taskId;
|
|
303
|
+
const configuration = settings().attachmentLifecycle;
|
|
304
|
+
if (event.finishReason !== "tool-calls" && event.message && configuration && principal && typeof taskId === "string") try {
|
|
305
|
+
const memberId = principal.attributes.memberId;
|
|
306
|
+
if (typeof memberId !== "string") throw new Error("A current member is required for completion.");
|
|
307
|
+
const result = await attachmentLifecycle({
|
|
308
|
+
configuration,
|
|
309
|
+
principal,
|
|
310
|
+
memberId,
|
|
311
|
+
environment,
|
|
312
|
+
token,
|
|
313
|
+
...options.fetch ? { fetch: options.fetch } : {},
|
|
314
|
+
message: {
|
|
315
|
+
chat: { id: channel.telegram.chatId },
|
|
316
|
+
messageId: String(principal.attributes.triggeringMessageId ?? principal.attributes.replyToMessageId),
|
|
317
|
+
messageThreadId: channel.telegram.messageThreadId,
|
|
318
|
+
from: { id: principal.subject ?? "" }
|
|
319
|
+
}
|
|
320
|
+
})("completed", { key: taskId });
|
|
321
|
+
if (result.message !== void 0 || result.complete === false) event = {
|
|
322
|
+
...event,
|
|
323
|
+
message: result.message ?? ""
|
|
324
|
+
};
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const reference = reportTelegramError("intake_completion_failed", error, {
|
|
327
|
+
sessionId: context.session.id,
|
|
328
|
+
taskId
|
|
329
|
+
});
|
|
330
|
+
await channel.telegram.post(failureText(reference));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
await replyDelivery(observed, () => telegramSendScope(environment, settings().connection))(event, channel, context);
|
|
334
|
+
}
|
|
288
335
|
}
|
|
289
336
|
});
|
|
290
337
|
const adapter = channel.adapter;
|
|
@@ -334,7 +381,7 @@ function segmentstreamTelegram(options) {
|
|
|
334
381
|
context.state.customerLocale = locale;
|
|
335
382
|
await withDeliveryLocale(locale, async () => {
|
|
336
383
|
await handler(event, context);
|
|
337
|
-
}, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
|
|
384
|
+
}, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId), typeof principal?.attributes.replyToMessageId === "string" ? principal.attributes.replyToMessageId : void 0, typeof principal?.attributes.chatId === "string" ? principal.attributes.chatId : void 0);
|
|
338
385
|
}]))
|
|
339
386
|
} },
|
|
340
387
|
routes: [...channel.routes.map((route) => gatedRoute({
|
|
@@ -365,6 +412,12 @@ function gatedRoute(input) {
|
|
|
365
412
|
let description;
|
|
366
413
|
const describe = () => description ??= args.describe();
|
|
367
414
|
const inbound = new InboundRequest(async () => (await describe()).description ?? "", async () => (await describe()).name);
|
|
415
|
+
if (options.settings()?.attachmentLifecycle !== void 0 && telegramWebhookVerifier(() => webhookSecret(environment))(request)) console.info("[segmentstream] telegram.phase", {
|
|
416
|
+
phase: "webhook_received",
|
|
417
|
+
traceId: inbound.traceId,
|
|
418
|
+
at: new Date().toISOString(),
|
|
419
|
+
runtime: agentBuild(environment)
|
|
420
|
+
});
|
|
368
421
|
const scoped = (address) => scopedAddress(address, options.sessions().scope);
|
|
369
422
|
inbound.sessions = {
|
|
370
423
|
resolveSession: args.resolveSession,
|
|
@@ -399,6 +452,7 @@ function gatedRoute(input) {
|
|
|
399
452
|
args.waitUntil(task);
|
|
400
453
|
};
|
|
401
454
|
inbound.waitUntil = waitUntil;
|
|
455
|
+
inbound.backgroundUntil = (task) => args.waitUntil(task);
|
|
402
456
|
return runInboundRequest(inbound, () => input.capture(request, async (routedRequest = request) => {
|
|
403
457
|
const response = await route.handler(routedRequest, {
|
|
404
458
|
...args,
|