@segmentstream/agent 0.8.0 → 0.8.2

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 (49) hide show
  1. package/README.md +52 -0
  2. package/dist/_chunks/build-metadata-DMTUHsRx.mjs +16 -0
  3. package/dist/_chunks/{inbound-request-BwBvXk0A.mjs → inbound-request-CYSoY6pu.mjs} +35 -5
  4. package/dist/extension/config.d.ts +3 -0
  5. package/dist/extension/config.mjs +17 -2
  6. package/dist/extension/lib/attachment-tools.d.ts +5 -4
  7. package/dist/extension/lib/attachment-tools.mjs +3 -3
  8. package/dist/extension/lib/build-metadata.d.ts +8 -0
  9. package/dist/extension/lib/build-metadata.mjs +8 -0
  10. package/dist/extension/lib/confirmations.mjs +1 -1
  11. package/dist/extension/lib/deferred-confirmations.mjs +2 -2
  12. package/dist/extension/lib/ontology-tools.d.ts +9 -2
  13. package/dist/extension/lib/ontology-tools.mjs +48 -7
  14. package/dist/extension/lib/questions.d.ts +2 -0
  15. package/dist/extension/lib/stored-files.d.ts +17 -0
  16. package/dist/extension/lib/stored-files.mjs +70 -0
  17. package/dist/extension/lib/telegram/adapter.mjs +58 -4
  18. package/dist/extension/lib/telegram/attachment-description.mjs +1 -1
  19. package/dist/extension/lib/telegram/attachments-send.mjs +3 -3
  20. package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
  21. package/dist/extension/lib/telegram/confirmation-text.mjs +1 -1
  22. package/dist/extension/lib/telegram/confirmations.mjs +2 -2
  23. package/dist/extension/lib/telegram/content.d.ts +1 -1
  24. package/dist/extension/lib/telegram/conversations.mjs +1 -1
  25. package/dist/extension/lib/telegram/customer-text.d.ts +1 -1
  26. package/dist/extension/lib/telegram/customer-text.mjs +1 -1
  27. package/dist/extension/lib/telegram/deferred-confirmations.mjs +2 -2
  28. package/dist/extension/lib/telegram/files.mjs +1 -1
  29. package/dist/extension/lib/telegram/health.mjs +3 -1
  30. package/dist/extension/lib/telegram/images.mjs +1 -1
  31. package/dist/extension/lib/telegram/inbound-request.d.ts +11 -0
  32. package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
  33. package/dist/extension/lib/telegram/inbound.d.ts +2 -0
  34. package/dist/extension/lib/telegram/inbound.mjs +146 -4
  35. package/dist/extension/lib/telegram/lifecycle.d.ts +46 -0
  36. package/dist/extension/lib/telegram/lifecycle.mjs +163 -0
  37. package/dist/extension/lib/telegram/media-policy.mjs +1 -1
  38. package/dist/extension/lib/telegram/silent.mjs +1 -1
  39. package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
  40. package/dist/extension/lib/telegram/voice.mjs +1 -1
  41. package/dist/extension/tools/attachments_discard.d.ts +4 -4
  42. package/dist/extension/tools/attachments_list.d.ts +4 -4
  43. package/dist/extension/tools/attachments_promote.d.ts +4 -4
  44. package/dist/extension/tools/attachments_promote.mjs +12 -7
  45. package/dist/extension/tools/attachments_read.mjs +17 -4
  46. package/dist/extension/tools/attachments_table.d.ts +4 -4
  47. package/dist/extension/tools/questions_ask.d.ts +3 -1
  48. package/dist/extension/tools/questions_ask.mjs +2 -0
  49. 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.2";
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 text = request.text === void 0 ? message : replaceText(message, request.text);
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 declare function attachmentForModel(attachment: Attachment): {
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: Attachment, bytes: Uint8Array, options?: ReadOptions): Promise<ReadOutput>;
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 === null ? {} : { promotion: 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-DMTUHsRx.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
- const tool = tools.find((candidate) => candidate.name === bareName);
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 toolFor(memberId, action);
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 { tool, tools } = await toolFor(memberId, action, signal);
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
- export { actionToolName, ontologyToolLabels, ontologyTools };
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-DMTUHsRx.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": replyDelivery(observed, () => telegramSendScope(environment, settings().connection))
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,