@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.
- package/dist/extension/lib/attachment-tools.d.ts +4 -3
- package/dist/extension/lib/attachment-tools.mjs +4 -2
- package/dist/extension/lib/attachments.d.ts +2 -2
- package/dist/extension/lib/confirmation-description.d.ts +25 -0
- package/dist/extension/lib/confirmation-description.mjs +81 -0
- package/dist/extension/lib/confirmations.d.ts +4 -0
- package/dist/extension/lib/confirmations.mjs +9 -2
- package/dist/extension/lib/conversations.d.ts +2 -0
- package/dist/extension/lib/conversations.mjs +15 -3
- package/dist/extension/lib/identities.d.ts +1 -0
- package/dist/extension/lib/identities.mjs +2 -1
- package/dist/extension/lib/ontology-connection.mjs +2 -2
- package/dist/extension/lib/ontology-tools.d.ts +6 -2
- package/dist/extension/lib/ontology-tools.mjs +95 -25
- package/dist/extension/lib/telegram/adapter.mjs +75 -40
- package/dist/extension/lib/telegram/attachments-send.d.ts +1 -0
- package/dist/extension/lib/telegram/attachments-send.mjs +24 -29
- package/dist/extension/lib/telegram/confirmation-text.d.ts +20 -0
- package/dist/extension/lib/telegram/confirmation-text.mjs +88 -0
- package/dist/extension/lib/telegram/confirmations.d.ts +2 -0
- package/dist/extension/lib/telegram/confirmations.mjs +173 -144
- package/dist/extension/lib/telegram/conversations.d.ts +1 -1
- package/dist/extension/lib/telegram/conversations.mjs +10 -5
- package/dist/extension/lib/telegram/customer-text.d.ts +20 -0
- package/dist/extension/lib/telegram/customer-text.mjs +120 -0
- package/dist/extension/lib/telegram/files.mjs +6 -5
- package/dist/extension/lib/telegram/inbound-request.d.ts +2 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
- package/dist/extension/lib/telegram/inbound.mjs +10 -4
- package/dist/extension/lib/telegram/principal.d.ts +3 -0
- package/dist/extension/lib/telegram/principal.mjs +11 -1
- package/dist/extension/lib/telegram/requests.mjs +5 -1
- package/dist/extension/lib/telegram/silent.mjs +5 -2
- package/dist/extension/lib/telegram/turn-errors.d.ts +2 -9
- package/dist/extension/lib/telegram/turn-errors.mjs +36 -10
- package/dist/extension/tools/attachments_promote.mjs +11 -2
- package/dist/extension/tools/attachments_send.d.ts +1 -0
- 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
|
|
9
|
-
* session's own id. A tool's arguments name only the attachment
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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 };
|
|
@@ -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 {
|
|
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(
|
|
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
|
|
5
|
+
export declare function ontologyTools(options?: {
|
|
6
6
|
readonly environment?: Environment;
|
|
7
7
|
readonly token?: RuntimeToken;
|
|
8
8
|
readonly fetch?: typeof fetch;
|
|
9
|
-
}):
|
|
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
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 =
|
|
46
|
+
const tool = tools.find((candidate) => candidate.name === bareName);
|
|
33
47
|
if (tool === void 0) throw new Error("Action not permitted.");
|
|
34
|
-
return
|
|
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 {
|
|
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 {
|
|
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) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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({
|