@segmentstream/agent 0.26.1 → 0.28.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 (62) hide show
  1. package/README.md +41 -43
  2. package/dist/_chunks/{build-metadata-D_Gqlb5f.mjs → build-metadata-CMmCRD8l.mjs} +1 -1
  3. package/dist/extension/config.d.ts +11 -12
  4. package/dist/extension/config.mjs +20 -37
  5. package/dist/extension/extension.d.ts +2 -2
  6. package/dist/extension/extension.mjs +3 -3
  7. package/dist/extension/hooks/chat-context.d.ts +3 -0
  8. package/dist/extension/hooks/chat-context.mjs +10 -0
  9. package/dist/extension/hooks/sandbox.d.ts +2 -3
  10. package/dist/extension/instructions/conversations.d.ts +3 -2
  11. package/dist/extension/instructions/conversations.mjs +7 -11
  12. package/dist/extension/instructions.md +18 -0
  13. package/dist/extension/lib/build-metadata.mjs +1 -1
  14. package/dist/extension/lib/chat-context.d.ts +108 -0
  15. package/dist/extension/lib/chat-context.mjs +217 -0
  16. package/dist/extension/lib/conversation-runtime.d.ts +1 -1
  17. package/dist/extension/lib/conversation-runtime.mjs +1 -23
  18. package/dist/extension/lib/conversations.d.ts +10 -6
  19. package/dist/extension/lib/conversations.mjs +28 -42
  20. package/dist/extension/lib/group-routing/addressing.d.ts +18 -0
  21. package/dist/extension/lib/group-routing/addressing.mjs +108 -0
  22. package/dist/extension/lib/group-routing/burst.d.ts +1 -1
  23. package/dist/extension/lib/group-routing/burst.mjs +1 -1
  24. package/dist/extension/lib/group-routing/conversation-prompt.d.ts +5 -2
  25. package/dist/extension/lib/group-routing/conversation-prompt.mjs +26 -6
  26. package/dist/extension/lib/group-routing/conversation-router.d.ts +8 -16
  27. package/dist/extension/lib/group-routing/conversation-router.mjs +49 -93
  28. package/dist/extension/lib/group-routing/dequeue.mjs +1 -1
  29. package/dist/extension/lib/group-routing/engagement.d.ts +1 -1
  30. package/dist/extension/lib/group-routing/{observed.d.ts → messages.d.ts} +9 -33
  31. package/dist/extension/lib/group-routing/messages.mjs +38 -0
  32. package/dist/extension/lib/group-routing/policy.d.ts +13 -9
  33. package/dist/extension/lib/group-routing/policy.mjs +35 -25
  34. package/dist/extension/lib/group-routing/router.d.ts +3 -11
  35. package/dist/extension/lib/group-routing/router.mjs +11 -35
  36. package/dist/extension/lib/group-routing/routing-context.d.ts +6 -2
  37. package/dist/extension/lib/group-routing/routing-context.mjs +12 -11
  38. package/dist/extension/lib/telegram/adapter.d.ts +1 -3
  39. package/dist/extension/lib/telegram/adapter.mjs +9 -18
  40. package/dist/extension/lib/telegram/confirmations.d.ts +3 -3
  41. package/dist/extension/lib/telegram/confirmations.mjs +3 -25
  42. package/dist/extension/lib/telegram/conversations.d.ts +14 -3
  43. package/dist/extension/lib/telegram/conversations.mjs +16 -37
  44. package/dist/extension/lib/telegram/delivery-diagnostics.d.ts +4 -1
  45. package/dist/extension/lib/telegram/delivery-diagnostics.mjs +9 -3
  46. package/dist/extension/lib/telegram/health.mjs +1 -1
  47. package/dist/extension/lib/telegram/inbound.d.ts +14 -14
  48. package/dist/extension/lib/telegram/inbound.mjs +73 -78
  49. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  50. package/dist/extension/lib/telegram/{observed.d.ts → message-metadata.d.ts} +2 -6
  51. package/dist/extension/lib/telegram/{observed.mjs → message-metadata.mjs} +1 -14
  52. package/dist/extension/lib/telegram/routing-context.d.ts +4 -3
  53. package/dist/extension/lib/telegram/routing-context.mjs +2 -2
  54. package/dist/extension/lib/telegram/sessions.d.ts +5 -52
  55. package/dist/extension/lib/telegram/sessions.mjs +1 -188
  56. package/dist/extension/lib/telegram/signals.mjs +1 -1
  57. package/dist/extension/lib/telegram/silent.d.ts +5 -6
  58. package/dist/extension/lib/telegram/silent.mjs +4 -31
  59. package/package.json +1 -1
  60. package/dist/extension/lib/group-routing/conversation.d.ts +0 -46
  61. package/dist/extension/lib/group-routing/conversation.mjs +0 -269
  62. package/dist/extension/lib/group-routing/observed.mjs +0 -103
@@ -32,192 +32,5 @@ function scopedAddress(address, scope) {
32
32
  conversationId: ""
33
33
  });
34
34
  }
35
- function localParts(timezone, date) {
36
- const parts = new Intl.DateTimeFormat("en-US", {
37
- timeZone: timezone,
38
- hourCycle: "h23",
39
- year: "numeric",
40
- month: "2-digit",
41
- day: "2-digit",
42
- hour: "2-digit",
43
- minute: "2-digit",
44
- second: "2-digit"
45
- }).formatToParts(date);
46
- const value = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
47
- return {
48
- year: value("year"),
49
- month: value("month"),
50
- day: value("day"),
51
- hour: value("hour"),
52
- minute: value("minute"),
53
- second: value("second")
54
- };
55
- }
56
- function offsetMinutes(timezone, date) {
57
- const local = localParts(timezone, date);
58
- const asUtc = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute, local.second);
59
- return Math.round((asUtc - date.getTime()) / 6e4);
60
- }
61
- function instantOf(timezone, local) {
62
- const guess = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute);
63
- const first = new Date(guess - offsetMinutes(timezone, new Date(guess)) * 6e4);
64
- const second = new Date(guess - offsetMinutes(timezone, first) * 6e4);
65
- const back = localParts(timezone, second);
66
- if (back.hour !== local.hour || back.minute !== local.minute) return first > second ? first : second;
67
- return second;
68
- }
69
- function compactBoundary(now, compactAt, timezone) {
70
- const [hour = 0, minute = 0] = compactAt.split(":").map(Number);
71
- const today = localParts(timezone, now);
72
- const candidate = instantOf(timezone, {
73
- ...today,
74
- hour,
75
- minute
76
- });
77
- if (candidate.getTime() <= now.getTime()) return candidate;
78
- const yesterday = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
79
- return instantOf(timezone, {
80
- year: yesterday.getUTCFullYear(),
81
- month: yesterday.getUTCMonth() + 1,
82
- day: yesterday.getUTCDate(),
83
- hour,
84
- minute
85
- });
86
- }
87
35
  const HISTORY_TAIL = 400;
88
- function historyOf(events) {
89
- let lastAt = 0;
90
- let resetAt = 0;
91
- let earliest = 0;
92
- let marked = false;
93
- let turnsSinceReset = 0;
94
- for (const event of events) {
95
- const at = Date.parse(event.meta.at);
96
- if (Number.isNaN(at)) continue;
97
- lastAt = Math.max(lastAt, at);
98
- earliest = earliest === 0 ? at : Math.min(earliest, at);
99
- if (event.type === "session.started" || event.type === "context.settled" && typeof event.data === "object" && event.data !== null && "outcome" in event.data && event.data.outcome === "completed") {
100
- resetAt = at;
101
- marked = true;
102
- turnsSinceReset = 0;
103
- } else if (event.type === "turn.settled") turnsSinceReset += 1;
104
- }
105
- return {
106
- lastAt,
107
- resetAt: marked ? resetAt : earliest,
108
- turnsSinceReset
109
- };
110
- }
111
- const EVIDENCE_OVERLAP_MS = 6e4;
112
- function evidenceSince(events) {
113
- const members = new Set();
114
- let since;
115
- for (const event of events) {
116
- const data = typeof event.data === "object" && event.data !== null ? event.data : {};
117
- if (event.type === "session.started" || event.type === "context.settled" && data.outcome === "completed") {
118
- members.clear();
119
- since = void 0;
120
- }
121
- if (event.type === "delivery.admitted" && typeof data.deliveryId === "string") {
122
- const principal = data.principal;
123
- const source = data.source;
124
- if (typeof principal?.id === "string" && principal.id.startsWith("telegram:") && source?.scheduleId === void 0) members.add(data.deliveryId);
125
- }
126
- const at = Date.parse(event.meta.at);
127
- if (event.type === "delivery.consumed" && members.has(String(data.deliveryId)) && Array.isArray(data.parts) && data.parts.length > 0 && !Number.isNaN(at)) since = at - EVIDENCE_OVERLAP_MS;
128
- }
129
- return since;
130
- }
131
- const MAXIMUM_REMEMBERED = 500;
132
- function sessionPolicy(input) {
133
- const remembered = new Map();
134
- const log = input.log ?? ((message, error) => console.warn(message, error));
135
- function remember(address, history) {
136
- remembered.delete(address);
137
- remembered.set(address, history);
138
- if (remembered.size > MAXIMUM_REMEMBERED) {
139
- const oldest = remembered.keys().next();
140
- if (oldest.done !== true) remembered.delete(oldest.value);
141
- }
142
- }
143
- async function history(address, operations, fresh = false) {
144
- const known = fresh ? void 0 : remembered.get(address);
145
- if (known !== void 0) return known;
146
- const session = await operations.resolveSession(address);
147
- if (session === void 0) {
148
- remembered.delete(address);
149
- return;
150
- }
151
- const read = historyOf(await input.read(session.id));
152
- remember(address, read);
153
- return read;
154
- }
155
- function due(known, settings, now, activeAt) {
156
- const idle = settings.idleResetHours;
157
- const lastAt = Math.max(known.lastAt, activeAt);
158
- if (idle !== false && now.getTime() - lastAt >= idle * 36e5) return "clear";
159
- if (settings.compactAt !== false) {
160
- const boundary = compactBoundary(now, settings.compactAt, settings.timezone).getTime();
161
- if (known.resetAt < boundary && known.turnsSinceReset > 0) return "compact";
162
- }
163
- return "none";
164
- }
165
- return {
166
- async apply({ address, operations, activeAt = 0, now = new Date() }) {
167
- const settings = input.settings();
168
- const cached = remembered.has(address);
169
- let known;
170
- try {
171
- known = await history(address, operations);
172
- } catch (error) {
173
- log("The session policy could not read the session's history; delivering as is.", error);
174
- return "none";
175
- }
176
- if (known === void 0) return "none";
177
- let action = due(known, settings, now, activeAt);
178
- if (action !== "none" && cached) {
179
- try {
180
- known = await history(address, operations, true);
181
- } catch (error) {
182
- log("The session policy could not re-read the session's history; delivering as is.", error);
183
- return "none";
184
- }
185
- if (known === void 0) return "none";
186
- action = due(known, settings, now, activeAt);
187
- }
188
- if (action === "clear") try {
189
- if ((await (await operations.from(address).clear()).result()).outcome !== "applied") {
190
- remembered.delete(address);
191
- return "none";
192
- }
193
- remember(address, {
194
- lastAt: known.lastAt,
195
- resetAt: now.getTime(),
196
- turnsSinceReset: 0
197
- });
198
- return "cleared";
199
- } catch (error) {
200
- log("The idle reset could not clear the session; delivering as is.", error);
201
- return "none";
202
- }
203
- if (action === "compact") try {
204
- await operations.from(address).compact();
205
- remembered.delete(address);
206
- return "compacted";
207
- } catch (error) {
208
- log("The daily compaction could not be queued; delivering as is.", error);
209
- }
210
- return "none";
211
- },
212
- delivered(address, now = new Date()) {
213
- const known = remembered.get(address);
214
- if (known === void 0) return;
215
- remember(address, {
216
- lastAt: now.getTime(),
217
- resetAt: known.resetAt,
218
- turnsSinceReset: known.turnsSinceReset + 1
219
- });
220
- }
221
- };
222
- }
223
- export { HISTORY_TAIL, compactBoundary, evidenceSince, formatAddress, historyOf, messageAddress, parseAddress, scopedAddress, sessionPolicy };
36
+ export { HISTORY_TAIL, formatAddress, messageAddress, parseAddress, scopedAddress };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { messageAudience } from "./audience.mjs";
8
- import { messageMetadata } from "./observed.mjs";
8
+ import { messageMetadata } from "./message-metadata.mjs";
9
9
  function telegramSignals(message, bot, settings) {
10
10
  const audience = messageAudience(message, bot);
11
11
  const meta = messageMetadata(message, bot);
@@ -1,5 +1,4 @@
1
1
  import type { TelegramChannelEvents } from "eve/channels/telegram";
2
- import { type ObservedMessages } from "./observed.js";
3
2
  export declare function isSilentReply(text: string): boolean;
4
3
  /** Use the same classification for delivery and its archive decision. Never strip real prose. */
5
4
  export declare function completionKind(message: string | null | undefined, finishReason?: string): "reply" | "tool_calls" | "empty" | "whitespace" | "silent_marker";
@@ -25,11 +24,11 @@ export interface InterimMessages {
25
24
  /**
26
25
  * The channel's final reply: formatted and split before Eve's plain-text splitter, with one
27
26
  * queue slot for the whole reply. Explicit silence markers have no delivery. Tool-call steps have
28
- * none either unless `interim` is enabled: then their text is delivered exactly like a reply, but
29
- * not remembered for group routing, which counts only final replies, as its recovery from the
30
- * session stream does. A text already delivered as an interim message in the same turn, whether
31
- * a repeat or the final reply, is not delivered again.
27
+ * none either unless `interim` is enabled: then their text is delivered exactly like a reply. The
28
+ * conversation archive records every delivery for routing and chat context. A text already
29
+ * delivered as an interim message in the same turn, whether a repeat or the final reply, is not
30
+ * delivered again.
32
31
  */
33
- export declare function replyDelivery(observed?: ObservedMessages, scope?: () => string, interim?: InterimMessages): MessageCompleted;
32
+ export declare function replyDelivery(scope?: () => string, interim?: InterimMessages): MessageCompleted;
34
33
  export declare const deliverUnlessSilent: MessageCompleted;
35
34
  export {};
@@ -4,12 +4,10 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import "../../../_chunks/principal-D99Lleae.mjs";
8
7
  import { confirmationLocale } from "./confirmation-text.mjs";
9
8
  import { splitTelegramText } from "./format.mjs";
10
9
  import { inTelegramSendQueue } from "./send-queue.mjs";
11
10
  import { a as TelegramDeliveryError, l as customerMessage, o as currentLocale, p as reportTelegramError } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
12
- import { agentMessage, observedChatKey } from "../group-routing/observed.mjs";
13
11
  const SILENT_MARKERS = new Set(["[silent]", "no_reply"]);
14
12
  function isSilentReply(text) {
15
13
  return SILENT_MARKERS.has(text.trim().toLowerCase());
@@ -36,7 +34,7 @@ function interimTexts(limit = 256) {
36
34
  }
37
35
  };
38
36
  }
39
- function replyDelivery(observed, scope = () => "telegram", interim) {
37
+ function replyDelivery(scope = () => "telegram", interim) {
40
38
  return async (event, context) => {
41
39
  if (event.data.kind !== "text" || event.data.interrupted) return;
42
40
  const channel = context.channel;
@@ -49,13 +47,12 @@ function replyDelivery(observed, scope = () => "telegram", interim) {
49
47
  const key = textValue.trim();
50
48
  if (turn !== void 0 && interim.delivered.has(turn, key)) return;
51
49
  const message = customerMessage(textValue, confirmationLocale(context.session?.auth.current ?? null, currentLocale()));
52
- const principal = context.session?.auth.current;
53
- const replyTo = principal?.attributes.replyToMessageId;
50
+ const replyTo = (context.session?.auth.current)?.attributes.replyToMessageId;
54
51
  const replyParameters = typeof replyTo === "string" && /^[1-9]\d*$/u.test(replyTo) ? { reply_parameters: {
55
52
  message_id: Number(replyTo),
56
53
  allow_sending_without_reply: true
57
54
  } } : {};
58
- const deliveredIds = await inTelegramSendQueue(scope(), channel.telegram.chatId, async () => {
55
+ await inTelegramSendQueue(scope(), channel.telegram.chatId, async () => {
59
56
  const messageIds = [];
60
57
  try {
61
58
  for (const part of splitTelegramText(message)) {
@@ -72,31 +69,7 @@ function replyDelivery(observed, scope = () => "telegram", interim) {
72
69
  }
73
70
  return messageIds;
74
71
  });
75
- const text = message.text;
76
- if (isInterim) {
77
- if (turn !== void 0) interim.delivered.add(turn, key);
78
- return;
79
- }
80
- if (observed === void 0 || channel.telegram.chatType === "private") return;
81
- const sender = principal?.authenticator === "segmentstream-telegram" ? principal.subject : void 0;
82
- const username = principal?.attributes.username;
83
- const name = principal?.attributes.memberName;
84
- const author = typeof name === "string" ? name : typeof username === "string" ? `@${username}` : sender;
85
- const inReplyTo = sender === void 0 || author === void 0 ? void 0 : {
86
- id: sender,
87
- author,
88
- ...typeof username === "string" ? { username } : {}
89
- };
90
- const chat = observedChatKey({
91
- chatId: channel.telegram.chatId,
92
- ...channel.telegram.messageThreadId === void 0 ? {} : { messageThreadId: channel.telegram.messageThreadId }
93
- });
94
- observed.remember(chat, agentMessage({
95
- text,
96
- at: new Date().toISOString(),
97
- messageId: String(deliveredIds.at(-1)),
98
- ...inReplyTo === void 0 ? {} : { inReplyTo }
99
- }));
72
+ if (isInterim && turn !== void 0) interim.delivered.add(turn, key);
100
73
  };
101
74
  }
102
75
  const deliverUnlessSilent = replyDelivery();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",
@@ -1,46 +0,0 @@
1
- import type { GroupPolicy } from "./policy.js";
2
- import type { GroupMessageSignals } from "./signals.js";
3
- import type { RecentMessage } from "./observed.js";
4
- export type ConversationPath = "A" | "H" | "C" | "Ci" | "C2" | "B" | "X";
5
- export interface ConversationState {
6
- status: "active" | "closed" | "none";
7
- opened_by: "explicit" | "agent_initiated" | "inferred" | null;
8
- opened_at?: string;
9
- last_exchange_at?: string;
10
- last_p_at?: string;
11
- contested: boolean;
12
- closed_reason?: "turned_to_human" | "idle" | "ended" | "inferred_expired";
13
- }
14
- export interface ConversationAnalysis {
15
- readonly path: ConversationPath;
16
- readonly conversation: ConversationState;
17
- readonly human_targets: ReadonlyArray<string>;
18
- readonly human_exchange: {
19
- readonly active: boolean;
20
- readonly with: ReadonlyArray<string>;
21
- readonly evidence: ReadonlyArray<string>;
22
- };
23
- readonly other_live_agent_conversations: ReadonlyArray<string>;
24
- readonly pending_question_live: boolean;
25
- }
26
- /** Workspace names are data. Only leading/trailing vocatives count as an address. */
27
- export declare function vocatives(text: string): ReadonlyArray<string>;
28
- export interface AddressEvidence {
29
- readonly agent_named: boolean;
30
- readonly reply_to_agent: boolean;
31
- readonly human_targets: ReadonlyArray<string>;
32
- readonly soft_target?: string;
33
- }
34
- export declare function conversationAddressing(signals: GroupMessageSignals, recent: ReadonlyArray<RecentMessage>, policy: GroupPolicy, agentNames?: ReadonlyArray<string>): AddressEvidence;
35
- export declare function recordedMessageSignals(m: RecentMessage, current: GroupMessageSignals): GroupMessageSignals;
36
- /** Restore bounded state from received messages and confirmed delivery facts, including unpaired humans. */
37
- export declare function conversationState(input: {
38
- readonly signals: GroupMessageSignals;
39
- readonly recent: ReadonlyArray<RecentMessage>;
40
- readonly policy: GroupPolicy;
41
- readonly agentNames?: ReadonlyArray<string>;
42
- readonly pendingQuestionAt?: string;
43
- /** Dequeued inputs use the current turn boundary without changing provider timestamps. */
44
- readonly asOf?: string;
45
- }): ConversationAnalysis;
46
- export declare const conversationThreshold: (path: ConversationPath, policy: GroupPolicy) => number;
@@ -1,269 +0,0 @@
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
- const own = (m) => "role" in m && m.role === "agent";
8
- const norm = (s) => s.normalize("NFKC").toLowerCase().replace(/^@/, "");
9
- const stamp = (s) => Date.parse(/[zZ]|[+-]\d\d:\d\d$/.test(s) ? s : s + "Z");
10
- function vocatives(text) {
11
- return [/^[\s«"“'([]*(@?[\p{L}\p{N}_-]{2,})(?=[\s,!:;?.»"”')\]]|$)/u.exec(text)?.[1], /,\s*([\p{L}\p{N}_-]{2,})\s*[?!.)»"”]*\s*$/u.exec(text)?.[1]].filter((s) => s !== void 0).map(norm);
12
- }
13
- function identities(participants, recent) {
14
- const names = new Map();
15
- for (const [id, p] of Object.entries(participants)) for (const a of [id, ...p.aliases]) names.set(norm(a), id);
16
- for (const m of recent) {
17
- if (own(m)) continue;
18
- const id = names.get(norm(m.authorId ?? m.username ?? m.author)) ?? m.authorId ?? m.username ?? m.author;
19
- for (const a of [
20
- m.authorId,
21
- m.username,
22
- m.author,
23
- m.author.split(/\s/u)[0]
24
- ]) if (a) names.set(norm(a), id);
25
- }
26
- return (name) => names.get(norm(name)) ?? norm(name);
27
- }
28
- function conversationAddressing(signals, recent, policy, agentNames = []) {
29
- const canonical = identities(policy.participants, recent);
30
- const sender = canonical(signals.sender.externalId);
31
- const agents = new Set([...agentNames, ...policy.smart.engagement.agentAliases].map(norm));
32
- const names = vocatives(signals.derived?.transcript ?? signals.message.text);
33
- const mentions = [...signals.addressing.mentions, ...Array.from(signals.message.text.matchAll(/(?:^|[^\p{L}\p{N}_])@([A-Za-z0-9_]+)/gu), (m) => ({
34
- username: m[1],
35
- kind: "unknown"
36
- }))];
37
- const punctuated = /^[\s«"“'([]*(@?[\p{L}\p{N}_-]{2,})\s*(?:[,!:?]|$)/u.exec(signals.derived?.transcript ?? signals.message.text)?.[1];
38
- const trailing = /,\s*([\p{L}\p{N}_-]{2,})\s*[?!.)»"”]*\s*$/u.exec(signals.derived?.transcript ?? signals.message.text)?.[1];
39
- const named = signals.addressing.agentMentioned || signals.addressing.command || [punctuated, trailing].some((n) => n !== void 0 && agents.has(norm(n))) || mentions.some((m) => m.kind === "bot" || agents.has(norm(m.username ?? m.externalId ?? "")));
40
- const humans = new Set();
41
- const terminalMention = /@([A-Za-z0-9_]+)\s*[.!?]*\s*$/u.exec(signals.message.text)?.[1];
42
- const replyToAgent = signals.addressing.replyTo?.author === "agent" || signals.addressing.threadWithAgent === true;
43
- let soft;
44
- for (const m of mentions) if (m.kind !== "bot" && !agents.has(norm(m.username ?? m.externalId ?? ""))) {
45
- const target = canonical(m.externalId ?? m.username ?? "unknown");
46
- if (replyToAgent && m.type !== "text_mention" && m.username !== void 0 && !names.some((name) => canonical(name) === target) && (terminalMention === void 0 || canonical(terminalMention) !== target)) soft = target;
47
- else humans.add(target);
48
- }
49
- const known = new Set([...Object.keys(policy.participants), ...recent.filter((m) => !own(m)).map((m) => canonical(m.authorId ?? m.username ?? m.author))]);
50
- for (const name of names) {
51
- const id = canonical(name);
52
- if (known.has(id) && id !== sender && !agents.has(name)) humans.add(id);
53
- }
54
- const reply = signals.addressing.replyTo;
55
- if (reply && reply.author !== "agent" && !reply.self) {
56
- const parent = recent.find((m) => m.messageId === reply.messageId);
57
- const id = canonical(reply.authorExternalId ?? reply.authorUsername ?? parent?.authorId ?? parent?.username ?? reply.authorName ?? "unknown");
58
- if (id !== sender) {
59
- const upload = reply.hasAttachments || (parent?.attachmentKinds?.length ?? 0) > 0;
60
- const parentTargetsSender = parent !== void 0 && ((parent.taggedUsers ?? []).some((m) => canonical(m.id ?? m.username ?? "") === sender) || vocatives(parent.text).some((n) => canonical(n) === sender));
61
- if (upload && !parentTargetsSender && reply.author !== "other_bot") soft = id;
62
- else humans.add(id);
63
- }
64
- }
65
- return {
66
- agent_named: named,
67
- reply_to_agent: replyToAgent,
68
- human_targets: [...humans],
69
- ...soft ? { soft_target: soft } : {}
70
- };
71
- }
72
- function recordedMessageSignals(m, current) {
73
- return {
74
- ...current,
75
- derived: void 0,
76
- sender: {
77
- externalId: m.authorId ?? m.username ?? m.author,
78
- username: m.username,
79
- displayName: m.author,
80
- kind: m.kind ?? "human"
81
- },
82
- message: {
83
- ...current.message,
84
- id: m.messageId ?? "",
85
- at: m.at,
86
- text: m.text,
87
- attachments: [],
88
- hasCaption: m.hasCaption ?? false
89
- },
90
- addressing: {
91
- agentMentioned: m.invitation === "mention" || (m.taggedUsers?.some((t) => t.kind === "bot") ?? false),
92
- command: m.invitation === "command",
93
- threadWithAgent: m.invitation === "thread-with-agent",
94
- mentions: (m.taggedUsers ?? []).map((t) => ({
95
- type: t.type,
96
- externalId: t.id,
97
- username: t.username,
98
- kind: t.kind
99
- })),
100
- ...m.replyTo ? { replyTo: {
101
- messageId: m.replyTo.messageId,
102
- author: m.replyTo.kind === "bot" ? "agent" : m.replyTo.kind,
103
- authorExternalId: m.replyTo.id,
104
- authorUsername: m.replyTo.username
105
- } } : {}
106
- }
107
- };
108
- }
109
- function conversationState(input) {
110
- const { signals, policy } = input;
111
- const now = stamp(input.asOf ?? signals.message.at);
112
- const settings = policy.smart.engagement;
113
- const recent = input.recent.filter((m) => m.messageId !== signals.message.id && stamp(m.at) <= now).slice().sort((a, b) => stamp(a.at) - stamp(b.at));
114
- const current = {
115
- author: signals.sender.displayName ?? signals.sender.externalId,
116
- authorId: signals.sender.externalId,
117
- username: signals.sender.username,
118
- at: signals.message.at,
119
- text: signals.derived?.transcript ?? signals.message.text,
120
- messageId: signals.message.id,
121
- kind: signals.sender.kind
122
- };
123
- const canonical = identities(policy.participants, [...recent, current]);
124
- const states = new Map();
125
- const edges = [];
126
- let lastAgent = -Infinity;
127
- const humanSinceAgent = [];
128
- const open = (person, by, at) => states.set(person, {
129
- status: "active",
130
- opened_by: by,
131
- opened_at: at,
132
- last_exchange_at: at,
133
- last_p_at: by === "agent_initiated" ? void 0 : at,
134
- contested: false
135
- });
136
- const expire = (state, t) => {
137
- if (state?.status !== "active") return;
138
- if (t - stamp(state.last_exchange_at) > settings.idleMinutes * 6e4) {
139
- state.status = "closed";
140
- state.closed_reason = "idle";
141
- } else if (state.opened_by === "inferred" && t - stamp(state.opened_at) > settings.inferredMaxMinutes * 6e4) {
142
- state.status = "closed";
143
- state.closed_reason = "inferred_expired";
144
- }
145
- };
146
- const human = (m, s, isCurrent) => {
147
- const t = stamp(isCurrent ? input.asOf ?? m.at : m.at), person = canonical(s.sender.externalId);
148
- const evidence = conversationAddressing(s, recent, policy, input.agentNames);
149
- for (const target of evidence.human_targets) {
150
- edges.push({
151
- at: t,
152
- from: person,
153
- to: target
154
- });
155
- const targetState = states.get(target);
156
- if (targetState?.status === "active") targetState.contested = true;
157
- }
158
- if (evidence.soft_target) edges.push({
159
- at: t,
160
- from: person,
161
- to: evidence.soft_target
162
- });
163
- for (const [other, state] of states) {
164
- expire(state, t);
165
- if (other !== person && state.status === "active" && evidence.human_targets.length === 0 && !evidence.agent_named && t > stamp(state.last_p_at ?? state.opened_at)) state.contested = true;
166
- }
167
- let state = states.get(person);
168
- expire(state, t);
169
- const wasActive = state?.status === "active";
170
- const explicit = evidence.agent_named || evidence.reply_to_agent && evidence.human_targets.length === 0 && evidence.soft_target === void 0;
171
- let path;
172
- if (explicit) {
173
- path = "A";
174
- open(person, "explicit", m.at);
175
- state = states.get(person);
176
- } else if (evidence.human_targets.length) {
177
- path = "H";
178
- if (state?.status === "active") {
179
- state.status = "closed";
180
- state.closed_reason = "turned_to_human";
181
- }
182
- } else if (state?.status === "active") path = state.contested || evidence.soft_target ? "C2" : state.opened_by === "inferred" ? "Ci" : "C";
183
- else path = "B";
184
- const humanEdges = edges.filter((e) => t - e.at >= 0 && t - e.at <= settings.humanExchangeMinutes * 6e4 && (e.from === person || e.to === person));
185
- const questions = edges.filter((e) => e.to === person && t - e.at <= settings.humanQuestionMinutes * 6e4 && lastAgent < e.at);
186
- const others = [...states].filter(([p, c]) => p !== person && c.status === "active" && t - stamp(c.last_p_at ?? c.opened_at) <= 12e4).map(([p]) => p);
187
- const sequence = [...humanSinceAgent.filter((x) => t - x.at <= settings.humanExchangeMinutes * 6e4).map((x) => x.who), person];
188
- const switches = sequence.slice(1).filter((p, i) => p !== sequence[i] && (p === person || sequence[i] === person)).length;
189
- const exchange = humanEdges.length > 0 || questions.length > 0 || switches >= 2 || others.length > 0;
190
- if (path === "B" && exchange) path = "X";
191
- if (!isCurrent) {
192
- if (m.conversationEnded && state) {
193
- state.status = "closed";
194
- state.closed_reason = "ended";
195
- }
196
- if (state?.status === "active" && [
197
- "A",
198
- "C",
199
- "Ci",
200
- "C2"
201
- ].includes(path)) state.last_p_at = m.at;
202
- humanSinceAgent.push({
203
- at: t,
204
- who: person
205
- });
206
- }
207
- return {
208
- path,
209
- conversation: { ...state ?? {
210
- status: "none",
211
- opened_by: null,
212
- contested: false
213
- } },
214
- human_targets: evidence.human_targets,
215
- human_exchange: {
216
- active: state?.status !== "active" && exchange,
217
- with: [...new Set([...humanEdges, ...questions].flatMap((e) => [e.from, e.to]).filter((p) => p !== person))],
218
- evidence: [
219
- ...humanEdges.length ? ["recent_human_address"] : [],
220
- ...questions.length ? ["human_question"] : [],
221
- ...switches >= 2 ? ["human_alternation"] : [],
222
- ...others.length ? ["other_live_agent_conversation"] : []
223
- ]
224
- },
225
- other_live_agent_conversations: others,
226
- pending_question_live: wasActive && state?.status === "active" && evidence.human_targets.length === 0 && input.pendingQuestionAt !== void 0 && t - stamp(input.pendingQuestionAt) >= 0 && (settings.openQuestionMinutes === "unlimited" || t - stamp(input.pendingQuestionAt) <= settings.openQuestionMinutes * 6e4)
227
- };
228
- };
229
- for (const m of recent) {
230
- if (!own(m)) {
231
- if (m.kind === "bot" || m.kind === "other_bot") continue;
232
- human(m, recordedMessageSignals(m, signals), false);
233
- continue;
234
- }
235
- if (m.confirmation) continue;
236
- lastAgent = stamp(m.at);
237
- humanSinceAgent.length = 0;
238
- const parent = recent.find((p) => !own(p) && p.messageId === m.replyTo?.messageId);
239
- const targets = new Set([...m.addressedTo.map((p) => canonical(p.id ?? p.username ?? p.author)), ...parent ? [canonical(parent.authorId ?? parent.username ?? parent.author)] : []]);
240
- const named = conversationAddressing(recordedMessageSignals({
241
- ...m,
242
- role: void 0
243
- }, signals), recent, policy, input.agentNames).human_targets;
244
- for (const target of named) targets.add(target);
245
- for (const target of targets) {
246
- const state = states.get(target);
247
- expire(state, stamp(m.at));
248
- if (state?.status === "active") state.last_exchange_at = m.at;
249
- else open(target, parent || m.inReplyTo ? "inferred" : "agent_initiated", m.at);
250
- }
251
- }
252
- return human(current, input.asOf === void 0 ? signals : {
253
- ...signals,
254
- message: {
255
- ...signals.message,
256
- at: input.asOf
257
- }
258
- }, true);
259
- }
260
- const conversationThreshold = (path, policy) => ({
261
- A: 0,
262
- H: 1,
263
- C: policy.smart.thresholds.continuation,
264
- Ci: policy.smart.thresholds.inferredContinuation,
265
- C2: policy.smart.thresholds.contested,
266
- B: policy.smart.thresholds.ambient,
267
- X: policy.smart.thresholds.humanExchange
268
- })[path];
269
- export { conversationAddressing, conversationState, conversationThreshold, recordedMessageSignals, vocatives };