@segmentstream/ontology 0.92.1 → 0.93.0

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 (35) hide show
  1. package/README.md +38 -1
  2. package/dist/analyze.d.ts +16 -1
  3. package/dist/analyze.js +52 -33
  4. package/dist/manifest.d.ts +87 -0
  5. package/dist/manifest.js +9 -0
  6. package/dist/migrate/model.d.ts +48 -0
  7. package/dist/runtime-source.d.ts +10 -2
  8. package/dist/runtime-source.js +18 -2
  9. package/dist/service/agent/attachments.js +12 -8
  10. package/dist/service/chat/delivery.d.ts +7 -0
  11. package/dist/service/chat/delivery.js +47 -25
  12. package/dist/service/chat/envelope.d.ts +26 -2
  13. package/dist/service/chat/providers/index.d.ts +22 -0
  14. package/dist/service/chat/providers/index.js +31 -0
  15. package/dist/service/chat/providers/provider.d.ts +61 -0
  16. package/dist/service/chat/providers/provider.js +28 -0
  17. package/dist/service/chat/providers/slack.d.ts +10 -0
  18. package/dist/service/chat/providers/slack.js +93 -0
  19. package/dist/service/chat/providers/telegram.d.ts +4 -12
  20. package/dist/service/chat/providers/telegram.js +29 -21
  21. package/dist/service/chat/providers/whatsapp.d.ts +14 -0
  22. package/dist/service/chat/providers/whatsapp.js +91 -0
  23. package/dist/service/chat/reads.d.ts +6 -1
  24. package/dist/service/chat/reads.js +9 -3
  25. package/dist/service/chat/resolve.d.ts +5 -1
  26. package/dist/service/chat/resolve.js +26 -19
  27. package/dist/service/chat/routes.js +6 -1
  28. package/dist/service/mcp/server.js +6 -1
  29. package/dist/service/mcp/tools.js +6 -1
  30. package/dist/service/schemas.d.ts +12 -0
  31. package/dist/slack.d.ts +15 -0
  32. package/dist/slack.js +21 -0
  33. package/dist/whatsapp.d.ts +14 -0
  34. package/dist/whatsapp.js +29 -0
  35. package/package.json +9 -1
@@ -5,7 +5,12 @@ import { CurrentPrincipal } from "../authentication.js";
5
5
  import type { OntologyBackend } from "../runtime-contract.js";
6
6
  import type { ConversationTransport } from "../agent/conversations.js";
7
7
  import type { ChatList, ChatHistory } from "./schemas.js";
8
- export declare function chatReads(backend: OntologyBackend["Service"], conversations?: ConversationTransport, attribution?: ChatAttributionSettings): {
8
+ export declare function chatReads(backend: OntologyBackend["Service"], options: {
9
+ /** The providers whose chats are listed, as `listedChatProviders` gives them. */
10
+ readonly providers: ReadonlyArray<string>;
11
+ readonly conversations?: ConversationTransport;
12
+ readonly attribution?: ChatAttributionSettings;
13
+ }): {
9
14
  list: (input: typeof ChatList.Type) => Effect.Effect<{
10
15
  items: {
11
16
  id: string;
@@ -5,13 +5,16 @@ import { CurrentPrincipal } from "../authentication.js";
5
5
  import { currentCaller } from "../caller-context.js";
6
6
  import { chatAttribution } from "./attribution.js";
7
7
  import { chatResolver, groupConversation, isChatMember } from "./resolve.js";
8
- export function chatReads(backend, conversations, attribution) {
8
+ /** The providers whose archived conversations are keyed by the chat a destination names. */
9
+ const HISTORY_PROVIDERS = ["telegram"];
10
+ export function chatReads(backend, options) {
11
+ const { providers, conversations, attribution } = options;
9
12
  return {
10
13
  list: (input) => Effect.gen(function* () {
11
14
  const principal = yield* CurrentPrincipal;
12
15
  const caller = yield* currentCaller;
13
16
  const query = input.query?.toLowerCase();
14
- const items = (yield* chatResolver(backend, principal).list(input.chat))
17
+ const items = (yield* chatResolver(backend, principal, providers).list(input.chat))
15
18
  .filter((item) => query === undefined || item.title.toLowerCase().includes(query))
16
19
  .sort((a, b) => a.id.localeCompare(b.id));
17
20
  const user = yield* backend.get({ entity: "user", id: principal.id });
@@ -39,7 +42,10 @@ export function chatReads(backend, conversations, attribution) {
39
42
  if (principal.kind !== "member") {
40
43
  return yield* new PermissionDenied({ message: "Only members may read conversations." });
41
44
  }
42
- const target = yield* chatResolver(backend, principal, "telegram", {
45
+ // History reads Telegram chats only. Other channels archive a conversation under their
46
+ // session's address with private coordinates, not under the chat a destination names, so
47
+ // their history would be incomplete until the archive keys them the same way.
48
+ const target = yield* chatResolver(backend, principal, HISTORY_PROVIDERS, {
43
49
  memberGroups: true,
44
50
  }).resolve(input.chat);
45
51
  // A group's members read its history, as they read its attachments; agent_chat:read reads
@@ -25,7 +25,11 @@ export declare function isChatMember(backend: OntologyBackend["Service"], princi
25
25
  * reads those chats' attachments: being in a chat is enough, `agent_chat:read` reads them all.
26
26
  */
27
27
  export declare function withChatMembership(transport: ConversationTransport, backend: OntologyBackend["Service"]): ConversationTransport;
28
- export declare function chatResolver(backend: OntologyBackend["Service"], principal: Principal, provider?: string, options?: {
28
+ /**
29
+ * Resolves the chats of `providers`: their group chats in the registry, and each member's private
30
+ * chat on each provider they have a live identity on, `<provider>:private:<member id>`.
31
+ */
32
+ export declare function chatResolver(backend: OntologyBackend["Service"], principal: Principal, providers: ReadonlyArray<string>, options?: {
29
33
  /** Also resolve the group chats a member is in without chat read or post permission. */
30
34
  readonly memberGroups?: boolean;
31
35
  }): {
@@ -65,7 +65,11 @@ export function withChatMembership(transport, backend) {
65
65
  cause,
66
66
  })), Effect.flatMap((memberGroups) => transport({ ...input, memberGroups })));
67
67
  }
68
- export function chatResolver(backend, principal, provider = "telegram", options = {}) {
68
+ /**
69
+ * Resolves the chats of `providers`: their group chats in the registry, and each member's private
70
+ * chat on each provider they have a live identity on, `<provider>:private:<member id>`.
71
+ */
72
+ export function chatResolver(backend, principal, providers, options = {}) {
69
73
  const canGroup = isAllowed("agent_chat:read", principal) || isAllowed("agent_chat:post_message", principal);
70
74
  const memberGroups = options.memberGroups === true && principal.kind === "member";
71
75
  const canDirectory = isAllowed("user:read", principal) || isAllowed("agent_chat:post_message", principal);
@@ -73,21 +77,22 @@ export function chatResolver(backend, principal, provider = "telegram", options
73
77
  if (!canGroup && principal.kind !== "member") {
74
78
  return yield* denied();
75
79
  }
76
- const privateId = chat?.startsWith(`${provider}:private:`)
77
- ? chat.slice(`${provider}:private:`.length)
78
- : undefined;
80
+ const named = providers.find((provider) => chat?.startsWith(`${provider}:private:`));
81
+ const privateId = named === undefined ? undefined : chat.slice(`${named}:private:`.length);
79
82
  const groupId = chat !== undefined && UUID.test(chat);
80
83
  const filters = [
84
+ { provider: { in: [...providers] } },
81
85
  ...(chat === undefined ? [] : [groupId ? { id: { eq: chat } } : { title: { eq: chat } }]),
82
86
  ...(canGroup ? [] : [memberOf(principal)]),
83
87
  ];
84
- const groups = (!canGroup && !memberGroups) || privateId !== undefined
88
+ const groups = (!canGroup && !memberGroups) || privateId !== undefined || providers.length === 0
85
89
  ? []
86
- : yield* records(backend, "agent_chat", filters.length === 0
87
- ? {}
88
- : { where: filters.length === 1 ? filters[0] : { and: filters } });
90
+ : yield* records(backend, "agent_chat", {
91
+ where: filters.length === 1 ? filters[0] : { and: filters },
92
+ });
89
93
  const result = groups
90
- .filter((row) => chat === undefined || row.id === chat || row.title === chat)
94
+ .filter((row) => providers.includes(String(row.provider)) &&
95
+ (chat === undefined || row.id === chat || row.title === chat))
91
96
  .map((row) => ({
92
97
  id: String(row.id),
93
98
  title: String(row.title),
@@ -119,16 +124,18 @@ export function chatResolver(backend, principal, provider = "telegram", options
119
124
  continue;
120
125
  }
121
126
  const identities = yield* backend.agents.listIdentities(member.principal.id);
122
- const identity = identities.find((item) => item.provider === provider && item.revokedAt === null);
123
- if (identity !== undefined) {
124
- result.push({
125
- id: `${provider}:private:${member.principal.id}`,
126
- title: member.name,
127
- provider,
128
- external_id: identity.externalId,
129
- kind: "private",
130
- participantIds: [member.principal.id],
131
- });
127
+ for (const provider of named === undefined ? providers : [named]) {
128
+ const identity = identities.find((item) => item.provider === provider && item.revokedAt === null);
129
+ if (identity !== undefined) {
130
+ result.push({
131
+ id: `${provider}:private:${member.principal.id}`,
132
+ title: member.name,
133
+ provider,
134
+ external_id: identity.externalId,
135
+ kind: "private",
136
+ participantIds: [member.principal.id],
137
+ });
138
+ }
132
139
  }
133
140
  }
134
141
  return result;
@@ -8,6 +8,7 @@ import { RequestId, failureResponse } from "../errors.js";
8
8
  import { OntologyBackend } from "../runtime-contract.js";
9
9
  import { identityToken, makeOperations } from "../operations.js";
10
10
  import { ConversationSearch } from "../agent/conversations.js";
11
+ import { listedChatProviders } from "./providers/index.js";
11
12
  import { chatReads } from "./reads.js";
12
13
  import { withChatMembership } from "./resolve.js";
13
14
  import { ChatList, ChatHistory } from "./schemas.js";
@@ -16,7 +17,11 @@ export function chatRoutes(options) {
16
17
  const router = yield* HttpRouter.HttpRouter;
17
18
  const backend = yield* OntologyBackend;
18
19
  const conversations = options.conversations && withChatMembership(options.conversations, backend);
19
- const reads = chatReads(backend, conversations, options.built.plan.ontology.config.chat?.attribution);
20
+ const reads = chatReads(backend, {
21
+ providers: listedChatProviders(options.built.plan.ontology.manifest.chat),
22
+ conversations,
23
+ attribution: options.built.plan.ontology.config.chat?.attribution,
24
+ });
20
25
  const operations = makeOperations(backend, options.built.plan, options.hooks, options.connections, options.replicate, options.chatDelivery);
21
26
  const planned = options.built.plan.entities.find((entity) => entity.name === "agent_chat");
22
27
  const action = planned.actions.find((candidate) => candidate.name === "post_message");
@@ -14,6 +14,7 @@ import { CONFIRMATION_PREVIEW_METHOD, previewLocale, withoutConfirmation } from
14
14
  import { consumeChatEnvelope, CurrentChatCall } from "../chat/envelope.js";
15
15
  import { ChatList, ChatHistory, ChatPostWire, RuntimeChatPostWire } from "../chat/schemas.js";
16
16
  import { ChatPostResult } from "../../chat.js";
17
+ import { listedChatProviders } from "../chat/providers/index.js";
17
18
  import { chatReads } from "../chat/reads.js";
18
19
  import { withChatMembership } from "../chat/resolve.js";
19
20
  import { catalogFor } from "../catalog.js";
@@ -831,7 +832,11 @@ export function mcpLayer(options) {
831
832
  const operations = makeOperations(backend, options.built.plan, options.hooks, options.connections, options.replicate, options.chatDelivery);
832
833
  const conversations = options.conversations && withChatMembership(options.conversations, backend);
833
834
  const server = {
834
- chats: chatReads(backend, conversations, options.built.plan.ontology.config.chat?.attribution),
835
+ chats: chatReads(backend, {
836
+ providers: listedChatProviders(options.built.plan.ontology.manifest.chat),
837
+ conversations,
838
+ attribution: options.built.plan.ontology.config.chat?.attribution,
839
+ }),
835
840
  built: options.built,
836
841
  tools,
837
842
  discovery: createDiscovery(options.built, tools),
@@ -9,6 +9,7 @@ import { actionInputJsonSchema } from "../../runtime/generated-actions.js";
9
9
  import { uploadFields } from "../../storage/attachments.js";
10
10
  import { MEMBER_FILE_SCHEMAS } from "../member-files.js";
11
11
  import { ConversationSearch } from "../agent/conversations.js";
12
+ import { declaredChatProviders } from "../chat/providers/index.js";
12
13
  import { runtimeAllows } from "./runtime-policy.js";
13
14
  import { confirmationFacts, needsConfirmation, withConfirmation, } from "./confirmation.js";
14
15
  import { ChatList, ChatListResult, ChatHistory, ChatPostWire, RuntimeChatPostWire, } from "../chat/schemas.js";
@@ -735,7 +736,11 @@ export function mcpTools(built, conversations = false, storage = {}, navigation
735
736
  ...original,
736
737
  inputSchema: encodedJsonSchema(ChatPostWire),
737
738
  outputSchema: encodedJsonSchema(ChatPostResult),
738
- ...(manifest.chat?.telegram === undefined ? {} : { _meta: { [CHAT_POST_META]: 1 } }),
739
+ // The same protocol for every provider: an agent that cannot post to one refuses its
740
+ // destinations when it describes the post.
741
+ ...(declaredChatProviders(manifest.chat).length === 0
742
+ ? {}
743
+ : { _meta: { [CHAT_POST_META]: 1 } }),
739
744
  annotations: {
740
745
  readOnlyHint: false,
741
746
  destructiveHint: false,
@@ -452,6 +452,12 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
452
452
  readonly telegram: Schema.optionalKey<Schema.Struct<{
453
453
  readonly connection: Schema.String;
454
454
  }>>;
455
+ readonly slack: Schema.optionalKey<Schema.Struct<{
456
+ readonly connection: Schema.String;
457
+ }>>;
458
+ readonly whatsapp: Schema.optionalKey<Schema.Struct<{
459
+ readonly connection: Schema.String;
460
+ }>>;
455
461
  readonly attribution: Schema.optionalKey<Schema.Struct<{
456
462
  readonly template: Schema.optionalKey<Schema.String>;
457
463
  readonly separator: Schema.optionalKey<Schema.String>;
@@ -681,6 +687,12 @@ export declare const Catalog: Schema.decodeTo<Schema.Struct<{
681
687
  readonly telegram: Schema.optionalKey<Schema.Struct<{
682
688
  readonly connection: Schema.String;
683
689
  }>>;
690
+ readonly slack: Schema.optionalKey<Schema.Struct<{
691
+ readonly connection: Schema.String;
692
+ }>>;
693
+ readonly whatsapp: Schema.optionalKey<Schema.Struct<{
694
+ readonly connection: Schema.String;
695
+ }>>;
684
696
  readonly attribution: Schema.optionalKey<Schema.Struct<{
685
697
  readonly template: Schema.optionalKey<Schema.String>;
686
698
  readonly separator: Schema.optionalKey<Schema.String>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Slack's text for an exact chat post. The ontology service sends it and the agent's
3
+ * confirmation computes it from the same function, so the digest the member approves and the
4
+ * text delivered are the same.
5
+ */
6
+ /** The most characters Slack accepts in `chat.postMessage`'s `markdown_text`. */
7
+ export declare const SLACK_MESSAGE_LIMIT = 12000;
8
+ /** Text that Slack's Markdown shows as it is: every character it could read as syntax escaped. */
9
+ export declare function escapeSlackMarkdown(text: string): string;
10
+ /**
11
+ * The member's exact Markdown, which Slack renders, then the separator and the attribution as
12
+ * literal text, the attribution in italics. Neither can open or close a construct of the member's
13
+ * Markdown.
14
+ */
15
+ export declare function renderSlack(markdown: string, attribution: string, separator?: string): string;
package/dist/slack.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Slack's text for an exact chat post. The ontology service sends it and the agent's
3
+ * confirmation computes it from the same function, so the digest the member approves and the
4
+ * text delivered are the same.
5
+ */
6
+ /** The most characters Slack accepts in `chat.postMessage`'s `markdown_text`. */
7
+ export const SLACK_MESSAGE_LIMIT = 12_000;
8
+ /** The ASCII punctuation Markdown can read as syntax, which a backslash makes literal. */
9
+ const MARKDOWN_SYNTAX = /[\\`*_~[\](){}<>#+\-.!|&]/gu;
10
+ /** Text that Slack's Markdown shows as it is: every character it could read as syntax escaped. */
11
+ export function escapeSlackMarkdown(text) {
12
+ return text.replace(MARKDOWN_SYNTAX, "\\$&");
13
+ }
14
+ /**
15
+ * The member's exact Markdown, which Slack renders, then the separator and the attribution as
16
+ * literal text, the attribution in italics. Neither can open or close a construct of the member's
17
+ * Markdown.
18
+ */
19
+ export function renderSlack(markdown, attribution, separator = "\n\n") {
20
+ return `${markdown}${escapeSlackMarkdown(separator)}_${escapeSlackMarkdown(attribution)}_`;
21
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * WhatsApp's text for an exact chat post. The ontology service sends it and the agent's
3
+ * confirmation computes it from the same function, so the digest the member approves and the
4
+ * text delivered are the same.
5
+ */
6
+ /** The most characters WhatsApp accepts in a text message's `body`. */
7
+ export declare const WHATSAPP_MESSAGE_LIMIT = 4096;
8
+ /** Text that WhatsApp shows as it is: every formatting marker replaced by its look-alike. */
9
+ export declare function literalWhatsAppText(text: string): string;
10
+ /**
11
+ * The member's text as written, then the separator and the attribution as literal text, the
12
+ * attribution in italics. Neither can open or close the member's formatting.
13
+ */
14
+ export declare function renderWhatsApp(text: string, attribution: string, separator?: string): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * WhatsApp's text for an exact chat post. The ontology service sends it and the agent's
3
+ * confirmation computes it from the same function, so the digest the member approves and the
4
+ * text delivered are the same.
5
+ */
6
+ /** The most characters WhatsApp accepts in a text message's `body`. */
7
+ export const WHATSAPP_MESSAGE_LIMIT = 4096;
8
+ /**
9
+ * WhatsApp's formatting markers (`*bold*`, `_italic_`, `~strikethrough~`, `` ` `` code) and
10
+ * the look-alike each becomes. WhatsApp has no escape character, so a marker in text the
11
+ * platform adds is replaced by a character WhatsApp never reads as formatting.
12
+ */
13
+ const MARKERS = {
14
+ "*": "∗",
15
+ _: "_",
16
+ "~": "∼",
17
+ "`": "ˋ",
18
+ };
19
+ /** Text that WhatsApp shows as it is: every formatting marker replaced by its look-alike. */
20
+ export function literalWhatsAppText(text) {
21
+ return text.replace(/[*_~`]/gu, (marker) => MARKERS[marker]);
22
+ }
23
+ /**
24
+ * The member's text as written, then the separator and the attribution as literal text, the
25
+ * attribution in italics. Neither can open or close the member's formatting.
26
+ */
27
+ export function renderWhatsApp(text, attribution, separator = "\n\n") {
28
+ return `${text}${literalWhatsAppText(separator)}_${literalWhatsAppText(attribution)}_`;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/ontology",
3
- "version": "0.92.1",
3
+ "version": "0.93.0",
4
4
  "description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",
@@ -29,6 +29,14 @@
29
29
  "types": "./dist/telegram.d.ts",
30
30
  "import": "./dist/telegram.js"
31
31
  },
32
+ "./slack": {
33
+ "types": "./dist/slack.d.ts",
34
+ "import": "./dist/slack.js"
35
+ },
36
+ "./whatsapp": {
37
+ "types": "./dist/whatsapp.d.ts",
38
+ "import": "./dist/whatsapp.js"
39
+ },
32
40
  "./channel-session": {
33
41
  "types": "./dist/channel-session.d.ts",
34
42
  "import": "./dist/channel-session.js"