@segmentstream/agent 0.5.0 → 0.5.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 (38) hide show
  1. package/dist/extension/lib/attachment-tools.d.ts +4 -3
  2. package/dist/extension/lib/attachment-tools.mjs +4 -2
  3. package/dist/extension/lib/attachments.d.ts +2 -2
  4. package/dist/extension/lib/confirmation-description.d.ts +25 -0
  5. package/dist/extension/lib/confirmation-description.mjs +81 -0
  6. package/dist/extension/lib/confirmations.d.ts +4 -0
  7. package/dist/extension/lib/confirmations.mjs +9 -2
  8. package/dist/extension/lib/conversations.d.ts +2 -0
  9. package/dist/extension/lib/conversations.mjs +15 -3
  10. package/dist/extension/lib/identities.d.ts +1 -0
  11. package/dist/extension/lib/identities.mjs +2 -1
  12. package/dist/extension/lib/ontology-connection.mjs +2 -2
  13. package/dist/extension/lib/ontology-tools.d.ts +6 -2
  14. package/dist/extension/lib/ontology-tools.mjs +95 -25
  15. package/dist/extension/lib/telegram/adapter.mjs +75 -40
  16. package/dist/extension/lib/telegram/attachments-send.d.ts +1 -0
  17. package/dist/extension/lib/telegram/attachments-send.mjs +24 -29
  18. package/dist/extension/lib/telegram/confirmation-text.d.ts +20 -0
  19. package/dist/extension/lib/telegram/confirmation-text.mjs +88 -0
  20. package/dist/extension/lib/telegram/confirmations.d.ts +2 -0
  21. package/dist/extension/lib/telegram/confirmations.mjs +173 -144
  22. package/dist/extension/lib/telegram/conversations.d.ts +1 -1
  23. package/dist/extension/lib/telegram/conversations.mjs +10 -5
  24. package/dist/extension/lib/telegram/customer-text.d.ts +20 -0
  25. package/dist/extension/lib/telegram/customer-text.mjs +120 -0
  26. package/dist/extension/lib/telegram/files.mjs +6 -5
  27. package/dist/extension/lib/telegram/inbound-request.d.ts +2 -0
  28. package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
  29. package/dist/extension/lib/telegram/inbound.mjs +10 -4
  30. package/dist/extension/lib/telegram/principal.d.ts +3 -0
  31. package/dist/extension/lib/telegram/principal.mjs +11 -1
  32. package/dist/extension/lib/telegram/requests.mjs +5 -1
  33. package/dist/extension/lib/telegram/silent.mjs +5 -2
  34. package/dist/extension/lib/telegram/turn-errors.d.ts +2 -9
  35. package/dist/extension/lib/telegram/turn-errors.mjs +36 -10
  36. package/dist/extension/tools/attachments_promote.mjs +11 -2
  37. package/dist/extension/tools/attachments_send.d.ts +1 -0
  38. package/package.json +1 -1
@@ -5,14 +5,15 @@ import { type Environment } from "./environment.js";
5
5
  import { type RuntimeToken } from "./runtime-token.js";
6
6
  /**
7
7
  * What the `segmentstream__attachments_*` tools share. The owner of every call comes from the turn:
8
- * the member and chat on the session's principal, which the channel set from the pairing, and the
9
- * session's own id. A tool's arguments name only the attachment and what to do with it.
8
+ * the member and chat on the session's principal, which the channel set from the pairing or a
9
+ * verified member request, and the session's own id. A tool's arguments name only the attachment
10
+ * and what to do with it.
10
11
  */
11
12
  /** Maximum characters in a read/query response; remaining data is available by continuation. */
12
13
  export declare const MAXIMUM_TEXT_CHARACTERS = 50000;
13
14
  /** The largest file `attachments_read` hands the model inline. */
14
15
  export declare const MAXIMUM_INLINE_BYTES: number;
15
- /** The turn has no member and chat to own attachments, such as a caller who never paired. */
16
+ /** The turn has no member and chat to authorize attachments. */
16
17
  export declare class AttachmentOwnerError extends Error {
17
18
  readonly name = "AttachmentOwnerError";
18
19
  }
@@ -17,9 +17,11 @@ var AttachmentOwnerError = class extends Error {
17
17
  name = "AttachmentOwnerError";
18
18
  };
19
19
  function attachmentOwner(input) {
20
- const memberId = memberIdOf(input.principal);
20
+ const attributes = input.principal?.attributes;
21
+ const requesterId = attributes?.requesterKind === "member" && attributes.chatType === "group" ? attributes.requesterId : void 0;
22
+ const memberId = memberIdOf(input.principal) ?? (typeof requesterId === "string" ? requesterId : void 0);
21
23
  const chatId = input.principal?.attributes.chatId;
22
- if (memberId === void 0 || typeof chatId !== "string" || chatId === "") throw new AttachmentOwnerError("Attachments are available only to a paired member in a chat with the agent.");
24
+ if (memberId === void 0 || memberId === "" || typeof chatId !== "string" || chatId === "") throw new AttachmentOwnerError("Attachments require a workspace member in a chat with the agent.");
23
25
  return {
24
26
  memberId,
25
27
  conversation: telegramConversation(chatId),
@@ -2,7 +2,7 @@ import type { RuntimeToken } from "./runtime-token.js";
2
2
  /**
3
3
  * The ontology service's attachment routes (`docs/agent.md`, "Attachments"). A file a member sends
4
4
  * the agent is stored once in the workspace's private file storage, for 24 hours, and belongs to
5
- * that member, the chat it arrived in and the agent session that first reads it. The agent calls
5
+ * the chat's authorized members in groups, or its sender and session in private chats. The agent calls
6
6
  * these routes as the deployment, with its OIDC token, naming the member, the chat and the session
7
7
  * from the turn's own context; nothing a model writes reaches these headers.
8
8
  */
@@ -14,7 +14,7 @@ export declare const CONVERSATION_HEADER = "X-SegmentStream-Agent-Conversation";
14
14
  export declare const SESSION_HEADER = "X-SegmentStream-Agent-Session";
15
15
  /** The URL protocol of a file part that names a stored attachment instead of a Telegram file. */
16
16
  export declare const ATTACHMENT_URL_PROTOCOL = "segmentstream-attachment:";
17
- /** Whose attachment it is, from the turn: the member, the chat and the agent session. */
17
+ /** Who asks for attachment access, from the turn: the member, chat and agent session. */
18
18
  export interface AttachmentOwner {
19
19
  readonly memberId: string;
20
20
  /** The chat, such as `telegram:12345`. */
@@ -0,0 +1,25 @@
1
+ /** Only human-facing facts cross into the confirmation renderer; identifiers stay in the gate. */
2
+ export interface ConfirmationDescription {
3
+ readonly action: string;
4
+ readonly entity?: string;
5
+ readonly description?: string;
6
+ readonly fields: readonly {
7
+ readonly label: string;
8
+ readonly value: string;
9
+ }[];
10
+ }
11
+ export declare function object(value: unknown): Record<string, unknown>;
12
+ export declare function humanize(value: string): string;
13
+ /** Reject machine identifiers and structured data even in authored labels or resolved titles. */
14
+ export declare function humanText(value: unknown, maximum?: number): string | undefined;
15
+ /** Resolve local schema references and wrappers without following remote references. */
16
+ export declare function fieldSchema(value: unknown, root: Record<string, unknown>, depth?: number): Record<string, unknown>;
17
+ export declare function describeConfirmation(input: {
18
+ readonly title?: string;
19
+ readonly description?: string;
20
+ readonly schema: Record<string, unknown>;
21
+ readonly entity?: Record<string, unknown>;
22
+ readonly entities: readonly Record<string, unknown>[];
23
+ readonly values: Readonly<Record<string, unknown>>;
24
+ readonly read: (entity: string, id: string) => Promise<Record<string, unknown> | undefined>;
25
+ }): Promise<ConfirmationDescription>;
@@ -0,0 +1,81 @@
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
+ function object(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
9
+ }
10
+ function humanize(value) {
11
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
12
+ }
13
+ function humanText(value, maximum = 180) {
14
+ if (typeof value !== "string") return;
15
+ const text = value.replace(/\s+/g, " ").trim();
16
+ if (text === "" || /[{}\[\]]|__|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|\b[0-9A-HJKMNP-TV-Z]{26}\b|\b[a-z0-9]{24,}\b/iu.test(text)) return;
17
+ return text.slice(0, maximum);
18
+ }
19
+ function fieldSchema(value, root, depth = 0) {
20
+ const schema = object(value);
21
+ if (depth > 12) return {};
22
+ const ref = schema.$ref;
23
+ const referenced = typeof ref === "string" && ref.startsWith("#/") ? ref.slice(2).split("/").reduce((at, key) => object(at)[key.replace(/~1/g, "/").replace(/~0/g, "~")], root) : {};
24
+ const branches = [
25
+ schema.allOf,
26
+ schema.anyOf,
27
+ schema.oneOf
28
+ ].flatMap((items) => Array.isArray(items) ? items.filter((item) => object(item).type !== "null") : []);
29
+ return Object.assign({}, ...ref === void 0 ? [] : [fieldSchema(referenced, root, depth + 1)], ...branches.map((branch) => fieldSchema(branch, root, depth + 1)), schema);
30
+ }
31
+ async function describeConfirmation(input) {
32
+ const entity = input.entity ?? {};
33
+ const properties = Array.isArray(entity.properties) ? entity.properties.map(object) : [];
34
+ const fields = [];
35
+ const read = new Map();
36
+ for (const [key, value] of Object.entries(input.values).slice(0, 20)) {
37
+ const declared = object(input.schema.properties)[key];
38
+ if (declared === void 0 || /password|secret|token|credential|authorization/i.test(key)) continue;
39
+ const schema = fieldSchema(declared, input.schema);
40
+ const itemSchema = schema.type === "array" ? fieldSchema(schema.items, input.schema) : schema;
41
+ const property = properties.find((candidate) => candidate.name === key);
42
+ const reference = itemSchema["x-ontology-ref"];
43
+ const target = input.entities.find((candidate) => candidate.name === reference);
44
+ const label = humanText(schema.title) ?? humanText(property?.label) ?? humanText(humanize(key));
45
+ if (label === void 0 || schema.writeOnly === true || schema.format === "password") continue;
46
+ const options = Array.isArray(property?.options) ? property.options.map(object) : [];
47
+ const choices = [itemSchema.oneOf, itemSchema.anyOf].flatMap((items) => Array.isArray(items) ? items.map(object) : []);
48
+ const values = [];
49
+ for (const item of (Array.isArray(value) ? value : [value]).slice(0, 5)) {
50
+ if (typeof reference === "string") {
51
+ let title;
52
+ if (target !== void 0 && typeof item === "string" && typeof target.title === "string") {
53
+ const cacheKey = `${reference}:${item}`;
54
+ if (!read.has(cacheKey)) read.set(cacheKey, input.read(reference, item));
55
+ const value = (await read.get(cacheKey))?.[target.title];
56
+ title = value === item ? void 0 : humanText(value);
57
+ }
58
+ values.push(title ?? humanText(target?.label) ?? label);
59
+ continue;
60
+ }
61
+ if (/^(id|.*_id)$/i.test(key) || itemSchema.format === "uuid" || typeof item === "object") continue;
62
+ const option = options.find((candidate) => candidate.value === item);
63
+ const choice = choices.find((candidate) => candidate.const === item);
64
+ const enumeration = Array.isArray(itemSchema.enum) || choices.length > 0 || options.length > 0;
65
+ const text = humanText(option?.label) ?? humanText(choice?.title) ?? (enumeration ? void 0 : typeof item === "string" ? humanText(humanize(item)) : typeof item === "number" || typeof item === "boolean" ? String(item) : void 0);
66
+ if (text !== void 0) values.push(text);
67
+ }
68
+ if (values.length > 0) fields.push({
69
+ label,
70
+ value: values.join(", ")
71
+ });
72
+ if (fields.length === 6) break;
73
+ }
74
+ return {
75
+ action: humanText(input.title) ?? "the requested action",
76
+ ...humanText(entity.label) === void 0 ? {} : { entity: humanText(entity.label) },
77
+ ...humanText(input.description?.split("\n\n")[0]) === void 0 ? {} : { description: humanText(input.description?.split("\n\n")[0]) },
78
+ fields
79
+ };
80
+ }
81
+ export { describeConfirmation, fieldSchema, humanText, humanize, object };
@@ -1,11 +1,13 @@
1
1
  import { type StateHandle } from "eve/context";
2
2
  import type { ApprovalConfiguration } from "eve/tools/approval";
3
+ import type { ConfirmationDescription } from "./confirmation-description.js";
3
4
  export declare const CONFIRMATION_TTL_MS: number;
4
5
  export interface ActionAnnotations {
5
6
  readonly readOnlyHint?: boolean;
6
7
  readonly destructiveHint?: boolean;
7
8
  readonly irreversibleHint?: boolean;
8
9
  readonly idempotentHint?: boolean;
10
+ readonly openWorldHint?: boolean;
9
11
  }
10
12
  /** Safety comes from the authenticated ontology, never from the name or model prose. */
11
13
  export declare function needsConfirmation(labels: ActionAnnotations): boolean;
@@ -16,11 +18,13 @@ export interface Confirmation {
16
18
  readonly inputHash: string;
17
19
  readonly expiresAt: number;
18
20
  readonly status: "pending" | "approved" | "consumed" | "cancelled";
21
+ readonly description?: ConfirmationDescription;
19
22
  }
20
23
  export declare const confirmations: StateHandle<Record<string, Confirmation>>;
21
24
  /** Eve pauses the original call and rechecks this policy immediately before executing it. */
22
25
  export declare function actionApproval(options: {
23
26
  readonly labels: (memberId: string, action: string, input: Readonly<Record<string, unknown>>) => Promise<ActionAnnotations>;
27
+ readonly describe?: (memberId: string, action: string, input: Readonly<Record<string, unknown>>) => Promise<ConfirmationDescription>;
24
28
  readonly state?: StateHandle<Record<string, Confirmation>>;
25
29
  readonly now?: () => number;
26
30
  }): ApprovalConfiguration;
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto";
9
9
  import { defineState } from "eve/context";
10
10
  const CONFIRMATION_TTL_MS = 6e5;
11
11
  function needsConfirmation(labels) {
12
- return labels.readOnlyHint !== true && (labels.destructiveHint === true || labels.irreversibleHint === true || labels.destructiveHint === void 0 && labels.irreversibleHint === void 0);
12
+ return labels.readOnlyHint !== true && (labels.destructiveHint === true || labels.irreversibleHint === true || labels.openWorldHint === true || labels.destructiveHint === void 0 && labels.irreversibleHint === void 0);
13
13
  }
14
14
  function canonical(value) {
15
15
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
@@ -59,6 +59,12 @@ function actionApproval(options) {
59
59
  }
60
60
  if (saved !== void 0) return denied("The original call is waiting for its Confirm / Cancel buttons.");
61
61
  if (!needsConfirmation(labels)) return "approved";
62
+ let description;
63
+ try {
64
+ description = await options.describe?.(memberId, ctx.toolName, ctx.toolInput ?? {});
65
+ } catch (error) {
66
+ console.warn("The confirmation description could not be read.", error);
67
+ }
62
68
  state.update((all) => ({
63
69
  ...all,
64
70
  [ctx.callId]: {
@@ -66,7 +72,8 @@ function actionApproval(options) {
66
72
  action: ctx.toolName,
67
73
  inputHash: hash,
68
74
  expiresAt: now() + CONFIRMATION_TTL_MS,
69
- status: "pending"
75
+ status: "pending",
76
+ ...description === void 0 ? {} : { description }
70
77
  }
71
78
  }));
72
79
  return "user-approval";
@@ -49,6 +49,8 @@ export interface Conversations {
49
49
  readonly limit?: number;
50
50
  }): Promise<ConversationPage>;
51
51
  }
52
+ /** Collapse delivery snapshots from older archives before building a user/model transcript. */
53
+ export declare function collapseConversationEvents(events: ConversationPage["events"]): ConversationPage["events"];
52
54
  export declare function conversationEvent(input: Pick<ConversationEvent, "eventId" | "provider" | "chatId" | "chatType" | "kind"> & Partial<ConversationEvent>): ConversationEvent;
53
55
  export declare function conversations(input?: {
54
56
  readonly environment?: Environment;
@@ -6,6 +6,18 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { proxyEnvironment } from "./environment.mjs";
8
8
  import { runtimeToken } from "./runtime-token.mjs";
9
+ function collapseConversationEvents(events) {
10
+ const latest = new Map();
11
+ for (const event of events) {
12
+ const key = event.kind === "outbound" && /^telegram:(send|edit):/u.test(event.eventId) ? event.eventId.replace(/:result$/u, "") : event.eventId;
13
+ const previous = latest.get(key);
14
+ if (previous === void 0 || (/^\d+$/u.test(event.sequence) && /^\d+$/u.test(previous.sequence) ? BigInt(event.sequence) > BigInt(previous.sequence) : event.storedAt > previous.storedAt)) latest.set(key, {
15
+ ...event,
16
+ eventId: key
17
+ });
18
+ }
19
+ return [...latest.values()];
20
+ }
9
21
  function conversationEvent(input) {
10
22
  return {
11
23
  threadId: null,
@@ -78,12 +90,12 @@ function summarizeArguments(value) {
78
90
  return (JSON.stringify(visit(value, 0)) ?? "{}").slice(0, 4e3);
79
91
  }
80
92
  async function storedConversationContext(archive, coordinates) {
81
- const messages = (await archive.context({
93
+ const messages = collapseConversationEvents((await archive.context({
82
94
  provider: coordinates.provider,
83
95
  chatId: coordinates.chatId,
84
96
  ...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
85
97
  limit: 100
86
- })).events.filter((event) => event.kind === "inbound" || event.kind === "outbound" && event.status === "completed");
98
+ })).events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && event.status === "completed");
87
99
  const selected = [];
88
100
  let characters = 0;
89
101
  for (const event of messages) {
@@ -104,4 +116,4 @@ async function storedConversationContext(archive, coordinates) {
104
116
  JSON.stringify(selected.reverse())
105
117
  ].join("\n");
106
118
  }
107
- export { conversationEvent, conversations, storedConversationContext, summarizeArguments };
119
+ export { collapseConversationEvents, conversationEvent, conversations, storedConversationContext, summarizeArguments };
@@ -11,6 +11,7 @@ export interface PairedMember {
11
11
  readonly id: string;
12
12
  readonly name: string | null;
13
13
  readonly roles: ReadonlyArray<string>;
14
+ readonly locale?: string;
14
15
  }
15
16
  export type IdentityLookup = {
16
17
  readonly status: "paired";
@@ -44,7 +44,8 @@ function pairedMember(value) {
44
44
  return {
45
45
  id: user.id,
46
46
  name: typeof user.name === "string" ? user.name : null,
47
- roles
47
+ roles,
48
+ ...typeof user.locale === "string" ? { locale: user.locale } : {}
48
49
  };
49
50
  }
50
51
  function pairing(value) {
@@ -8,7 +8,7 @@ import { appOrigin } from "./environment.mjs";
8
8
  import { memberIdOf } from "./telegram/principal.mjs";
9
9
  import { actionApproval } from "./confirmations.mjs";
10
10
  import { runtimeToken, tokenExpiry } from "./runtime-token.mjs";
11
- import { ontologyToolLabels } from "./ontology-tools.mjs";
11
+ import { ontologyTools } from "./ontology-tools.mjs";
12
12
  import { defineMcpClientConnection } from "eve/connections";
13
13
  const ACT_AS_HEADER = "X-SegmentStream-Act-As";
14
14
  const DESCRIPTION = "The company's ontology: its records, their history, and the actions that change them, narrowed to what the member you act for may read and do. Call describe for the catalog and me for the member.";
@@ -30,7 +30,7 @@ function ontologyConnection(input) {
30
30
  url: `${origin}/mcp`,
31
31
  description: DESCRIPTION,
32
32
  instanceKey: `${origin}/mcp#member:${memberId}`,
33
- approval: actionApproval({ labels: ontologyToolLabels(input) }),
33
+ approval: actionApproval(ontologyTools(input)),
34
34
  auth: {
35
35
  credentialOwner: "app",
36
36
  getToken: async () => {
@@ -2,10 +2,14 @@ import type { ActionAnnotations } from "./confirmations.js";
2
2
  import { type Environment } from "./environment.js";
3
3
  import { type RuntimeToken } from "./runtime-token.js";
4
4
  /** A fresh, permission-filtered catalog. Listing tools cannot run an action's prepare step. */
5
- export declare function ontologyToolLabels(options?: {
5
+ export declare function ontologyTools(options?: {
6
6
  readonly environment?: Environment;
7
7
  readonly token?: RuntimeToken;
8
8
  readonly fetch?: typeof fetch;
9
- }): (memberId: string, action: string) => Promise<ActionAnnotations>;
9
+ }): {
10
+ labels(memberId: string, action: string): Promise<ActionAnnotations>;
11
+ describe(memberId: string, action: string, input: Readonly<Record<string, unknown>>): Promise<import("./confirmation-description.js").ConfirmationDescription>;
12
+ };
13
+ export declare function ontologyToolLabels(options?: Parameters<typeof ontologyTools>[0]): (memberId: string, action: string) => Promise<ActionAnnotations>;
10
14
  /** MCP normalizes an authored camelCase action to snake_case. */
11
15
  export declare function actionToolName(entity: string, action: string): string;
@@ -5,37 +5,107 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin } from "./environment.mjs";
8
+ import { describeConfirmation, object } from "./confirmation-description.mjs";
9
+ import { redactDiagnostic } from "./diagnostics.mjs";
8
10
  import { runtimeToken } from "./runtime-token.mjs";
9
- function ontologyToolLabels(options = {}) {
10
- return async (memberId, action) => {
11
- const response = await (options.fetch ?? fetch)(`${appOrigin(options.environment ?? process.env)}/mcp`, {
12
- method: "POST",
13
- headers: {
14
- authorization: `Bearer ${await (options.token ?? runtimeToken)()}`,
15
- "X-SegmentStream-Act-As": memberId,
16
- "content-type": "application/json",
17
- accept: "application/json, text/event-stream",
18
- "mcp-protocol-version": "2025-11-25"
19
- },
20
- body: JSON.stringify({
21
- jsonrpc: "2.0",
22
- id: "approval-tools",
23
- method: "tools/list",
24
- params: {}
25
- }),
26
- signal: AbortSignal.timeout(15e3),
27
- redirect: "error"
28
- });
29
- if (!response.ok) throw new Error("Cannot check action permission.");
30
- const body = await response.json();
11
+ function ontologyTools(options = {}) {
12
+ async function rpc(memberId, method, params, signal) {
13
+ const credential = await (options.token ?? runtimeToken)();
14
+ const secrets = new Set([credential]);
15
+ try {
16
+ const response = await (options.fetch ?? fetch)(`${appOrigin(options.environment ?? process.env)}/mcp`, {
17
+ method: "POST",
18
+ headers: {
19
+ authorization: `Bearer ${credential}`,
20
+ "X-SegmentStream-Act-As": memberId,
21
+ "content-type": "application/json",
22
+ accept: "application/json, text/event-stream",
23
+ "mcp-protocol-version": "2025-11-25"
24
+ },
25
+ body: JSON.stringify({
26
+ jsonrpc: "2.0",
27
+ id: "approval-tools",
28
+ method,
29
+ params
30
+ }),
31
+ signal: signal ?? AbortSignal.timeout(15e3),
32
+ redirect: "error"
33
+ });
34
+ if (!response.ok) throw new Error(`Cannot check action permission (HTTP ${response.status}).`);
35
+ const body = object(await response.json());
36
+ if (body.error !== void 0) throw new Error("Ontology metadata unavailable.", { cause: body.error });
37
+ return object(body.result);
38
+ } catch (error) {
39
+ throw new Error("The ontology metadata request failed.", { cause: redactDiagnostic(error, secrets) });
40
+ }
41
+ }
42
+ async function toolFor(memberId, action, signal) {
43
+ const result = await rpc(memberId, "tools/list", {}, signal);
44
+ const tools = Array.isArray(result.tools) ? result.tools.map(object) : [];
31
45
  const bareName = action.split("__").at(-1);
32
- const tool = body.result?.tools?.find((candidate) => candidate.name === bareName);
46
+ const tool = tools.find((candidate) => candidate.name === bareName);
33
47
  if (tool === void 0) throw new Error("Action not permitted.");
34
- return tool.annotations ?? {};
48
+ return {
49
+ tool,
50
+ tools
51
+ };
52
+ }
53
+ return {
54
+ async labels(memberId, action) {
55
+ return object((await toolFor(memberId, action)).tool.annotations);
56
+ },
57
+ async describe(memberId, action, input) {
58
+ const signal = AbortSignal.timeout(15e3);
59
+ const { tool, tools } = await toolFor(memberId, action, signal);
60
+ let entities = [];
61
+ try {
62
+ const catalog = await rpc(memberId, "resources/read", { uri: "ontology://catalog" }, signal);
63
+ const text = object(Array.isArray(catalog.contents) ? catalog.contents[0] : void 0).text;
64
+ const manifest = object(typeof text === "string" ? JSON.parse(text) : void 0);
65
+ entities = Array.isArray(manifest.entities) ? manifest.entities.map(object) : [];
66
+ } catch (error) {
67
+ console.warn("The confirmation catalog could not be read.", error);
68
+ }
69
+ const entity = entities.find((candidate) => {
70
+ return [
71
+ "create",
72
+ "update",
73
+ "delete",
74
+ "link",
75
+ "unlink",
76
+ ...Array.isArray(candidate.actions) ? candidate.actions.map((item) => object(item).name) : []
77
+ ].some((name) => typeof name === "string" && actionToolName(String(candidate.name), name) === tool.name);
78
+ });
79
+ return describeConfirmation({
80
+ title: typeof tool.title === "string" ? tool.title : void 0,
81
+ description: typeof tool.description === "string" ? tool.description : void 0,
82
+ schema: object(tool.inputSchema),
83
+ entity,
84
+ entities,
85
+ values: input,
86
+ async read(entityName, id) {
87
+ const name = actionToolName(entityName, "get");
88
+ if (!tools.some((candidate) => candidate.name === name && object(candidate.annotations).readOnlyHint === true)) return;
89
+ try {
90
+ const result = await rpc(memberId, "tools/call", {
91
+ name,
92
+ arguments: { id }
93
+ }, signal);
94
+ return result.isError === true ? void 0 : object(result.structuredContent);
95
+ } catch (error) {
96
+ console.warn("A confirmation record title could not be read.", error);
97
+ return;
98
+ }
99
+ }
100
+ });
101
+ }
35
102
  };
36
103
  }
104
+ function ontologyToolLabels(options = {}) {
105
+ return ontologyTools(options).labels;
106
+ }
37
107
  function actionToolName(entity, action) {
38
108
  const snake = (name) => name.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
39
109
  return `${snake(entity)}_${snake(action)}`;
40
110
  }
41
- export { actionToolName, ontologyToolLabels };
111
+ export { actionToolName, ontologyToolLabels, ontologyTools };
@@ -5,6 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
8
+ import { conversationLanguage } from "./principal.mjs";
8
9
  import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
9
10
  import { runtimeToken } from "../runtime-token.mjs";
10
11
  import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
@@ -12,11 +13,13 @@ import { agentIdentities } from "../identities.mjs";
12
13
  import { conversations } from "../conversations.mjs";
13
14
  import { observedMessages } from "./observed.mjs";
14
15
  import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
16
+ import { failureText, reportTelegramError, safeTelegramFetch, withDeliveryLocale } from "./customer-text.mjs";
15
17
  import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
16
18
  import { runtimeTokenVerifier } from "../runtime-identity.mjs";
17
19
  import { botIdentityCache } from "./bot.mjs";
20
+ import { confirmationLocale, confirmationRenderer } from "./confirmation-text.mjs";
18
21
  import { scopedAddress, sessionPolicy } from "./sessions.mjs";
19
- import { confirmationEvents, handleConfirmationCallback } from "./confirmations.mjs";
22
+ import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
20
23
  import { agentChats } from "./chats.mjs";
21
24
  import { decisionRouter } from "./router.mjs";
22
25
  import { replyDelivery } from "./silent.mjs";
@@ -25,7 +28,7 @@ import { handleInboundMessage } from "./inbound.mjs";
25
28
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
26
29
  import { messagesRoute } from "./requests.mjs";
27
30
  import { healthRoute } from "./health.mjs";
28
- import { withTurnErrorLogging } from "./turn-errors.mjs";
31
+ import { telegramFailureEvents } from "./turn-errors.mjs";
29
32
  import { telegramWebhookVerifier } from "./webhook.mjs";
30
33
  import { telegramChannel } from "eve/channels/telegram";
31
34
  const eveHistory = async (sessionId) => {
@@ -121,56 +124,81 @@ function segmentstreamTelegram(options) {
121
124
  api: {
122
125
  apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
123
126
  fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
124
- fetch: (input, init) => captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing)(input, init)
127
+ fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing))(input, init)
125
128
  },
126
129
  credentials: {
127
130
  botToken: TELEGRAM_PLACEHOLDER_TOKEN,
128
131
  webhookVerifier: telegramWebhookVerifier(() => webhookSecret(environment))
129
132
  },
130
133
  uploadPolicy: { maxBytes: MAXIMUM_DOWNLOAD_BYTES },
131
- onMessage: (context, message) => handleInboundMessage({
132
- context,
133
- message,
134
- request: currentInboundRequest(),
135
- dependencies: {
136
- policy: settings().policy,
137
- sessions: options.sessions,
138
- sessionPolicy: policy,
139
- sttModel: options.models().stt,
140
- identities: conversationIdentities(pairing()),
141
- bot,
142
- decide: captureRouteDecider(decisionRouter({
143
- decide: options.decide,
144
- model: options.models().decision
145
- })),
146
- transcribe: options.transcribe ?? gatewayTranscriber,
147
- download: (fileId) => downloadTelegramFile({
148
- proxy: proxy(),
149
- fileId
150
- }),
151
- attachments: attachments(),
152
- observed,
153
- readHistory,
154
- history: readInteractions,
155
- chats,
156
- connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
134
+ onMessage: async (context, message) => {
135
+ const inbound = currentInboundRequest();
136
+ if (inbound !== void 0) {
137
+ inbound.locale = conversationLanguage([message.text || message.caption]) ?? message.from?.languageCode ?? "en";
138
+ inbound.threadId = message.messageThreadId === void 0 ? void 0 : String(message.messageThreadId);
157
139
  }
158
- }),
159
- onCallbackQuery: (context, query) => handleConfirmationCallback({
160
- context,
161
- query,
162
- identities: pairing(),
163
- read: readInteractions,
164
- scope: options.sessions().scope
165
- }),
140
+ try {
141
+ const result = await handleInboundMessage({
142
+ context,
143
+ message,
144
+ request: currentInboundRequest(),
145
+ dependencies: {
146
+ policy: settings().policy,
147
+ sessions: options.sessions,
148
+ sessionPolicy: policy,
149
+ sttModel: options.models().stt,
150
+ identities: conversationIdentities(pairing()),
151
+ bot,
152
+ decide: captureRouteDecider(decisionRouter({
153
+ decide: options.decide,
154
+ model: options.models().decision
155
+ })),
156
+ transcribe: options.transcribe ?? gatewayTranscriber,
157
+ download: (fileId) => downloadTelegramFile({
158
+ proxy: proxy(),
159
+ fileId
160
+ }),
161
+ attachments: attachments(),
162
+ observed,
163
+ readHistory,
164
+ history: readInteractions,
165
+ chats,
166
+ connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
167
+ }
168
+ });
169
+ if (inbound !== void 0 && result !== null) inbound.locale = confirmationLocale(result.auth);
170
+ return result;
171
+ } catch (error) {
172
+ const errorId = reportTelegramError("inbound_message_failed", error, { chatId: message.chat.id });
173
+ await context.telegram.post(failureText(errorId));
174
+ return null;
175
+ }
176
+ },
177
+ onCallbackQuery: async (context, query) => {
178
+ try {
179
+ await handleConfirmationCallback({
180
+ context,
181
+ query,
182
+ identities: pairing(),
183
+ read: readInteractions,
184
+ scope: options.sessions().scope
185
+ });
186
+ } catch (error) {
187
+ const errorId = reportTelegramError("confirmation_callback_failed", error);
188
+ await context.telegram.answerCallbackQuery({
189
+ callbackQueryId: query.id,
190
+ text: failureText(errorId)
191
+ });
192
+ }
193
+ },
166
194
  events: {
167
- ...confirmationEvents,
195
+ ...telegramFailureEvents,
196
+ ...telegramConfirmationEvents(confirmationRenderer({ model: () => options.models().decision })),
168
197
  "message.completed": replyDelivery(observed)
169
198
  }
170
199
  });
171
200
  const adapter = channel.adapter;
172
201
  const fetchTelegramFile = adapter?.fetchFile;
173
- const turnFailed = adapter?.["turn.failed"];
174
202
  async function fetchFile(url, context) {
175
203
  const reference = parseAttachmentUrl(url);
176
204
  if (reference === void 0) return fetchTelegramFile === void 0 ? null : fetchTelegramFile(url, context);
@@ -191,7 +219,14 @@ function segmentstreamTelegram(options) {
191
219
  ...adapter === void 0 ? {} : { adapter: {
192
220
  ...adapter,
193
221
  fetchFile,
194
- ...turnFailed === void 0 ? {} : { "turn.failed": withTurnErrorLogging(turnFailed) }
222
+ ...Object.fromEntries(Object.entries(adapter).filter(([name, handler]) => name.includes(".") && typeof handler === "function").map(([name, handler]) => [name, async (event, context) => {
223
+ const principal = context.session?.auth.current;
224
+ const locale = confirmationLocale(principal ?? null, context.state.customerLocale ?? "en");
225
+ context.state.customerLocale = locale;
226
+ await withDeliveryLocale(locale, async () => {
227
+ await handler(event, context);
228
+ }, context.state.messageThreadId === null ? void 0 : String(context.state.messageThreadId));
229
+ }]))
195
230
  } },
196
231
  routes: [
197
232
  ...channel.routes.map((route) => gatedRoute({
@@ -37,4 +37,5 @@ export declare function sendAttachments(options: {
37
37
  } | {
38
38
  readonly error: string;
39
39
  readonly detail: string;
40
+ readonly reference: string;
40
41
  }>;