@segmentstream/agent 0.8.23 → 0.9.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 (64) hide show
  1. package/dist/_chunks/{build-metadata-26dmE9nn.mjs → build-metadata-DuHeGf1m.mjs} +1 -1
  2. package/dist/_chunks/content-CvFsrmSD.mjs +102 -0
  3. package/dist/extension/channels/telegram.mjs +1 -0
  4. package/dist/extension/config.d.ts +16 -2
  5. package/dist/extension/config.mjs +92 -3
  6. package/dist/extension/extension.d.ts +7 -0
  7. package/dist/extension/extension.mjs +8 -1
  8. package/dist/extension/hooks/conversations.mjs +1 -1
  9. package/dist/extension/instructions/conversations.mjs +1 -1
  10. package/dist/extension/lib/build-metadata.mjs +1 -1
  11. package/dist/extension/lib/conversation-runtime.mjs +132 -1
  12. package/dist/extension/lib/deferred-confirmations.mjs +1 -1
  13. package/dist/extension/lib/group-routing/engagement.d.ts +18 -0
  14. package/dist/extension/lib/group-routing/engagement.mjs +65 -0
  15. package/dist/extension/lib/group-routing/health.d.ts +11 -0
  16. package/dist/extension/lib/group-routing/health.mjs +36 -0
  17. package/dist/extension/lib/group-routing/observed.d.ts +100 -0
  18. package/dist/extension/lib/group-routing/observed.mjs +103 -0
  19. package/dist/extension/lib/group-routing/policy.d.ts +63 -0
  20. package/dist/extension/lib/group-routing/policy.mjs +162 -0
  21. package/dist/extension/lib/group-routing/record.d.ts +3 -0
  22. package/dist/extension/lib/group-routing/record.mjs +35 -0
  23. package/dist/extension/lib/group-routing/replay.d.ts +18 -0
  24. package/dist/extension/lib/group-routing/replay.mjs +70 -0
  25. package/dist/extension/lib/group-routing/router.d.ts +109 -0
  26. package/dist/extension/lib/group-routing/router.mjs +383 -0
  27. package/dist/extension/lib/group-routing/routing-context.d.ts +4 -0
  28. package/dist/extension/lib/group-routing/routing-context.mjs +96 -0
  29. package/dist/extension/lib/group-routing/settings.d.ts +14 -0
  30. package/dist/extension/lib/group-routing/settings.mjs +58 -0
  31. package/dist/extension/lib/group-routing/signals.d.ts +110 -0
  32. package/dist/extension/lib/group-routing/signals.mjs +47 -0
  33. package/dist/extension/lib/identities.d.ts +3 -0
  34. package/dist/extension/lib/identities.mjs +13 -1
  35. package/dist/extension/lib/ontology-connection.mjs +1 -1
  36. package/dist/extension/lib/ontology-tools.mjs +1 -1
  37. package/dist/extension/lib/telegram/adapter.d.ts +2 -0
  38. package/dist/extension/lib/telegram/adapter.mjs +17 -5
  39. package/dist/extension/lib/telegram/attachments-send.mjs +1 -1
  40. package/dist/extension/lib/telegram/audience.d.ts +3 -26
  41. package/dist/extension/lib/telegram/audience.mjs +1 -5
  42. package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
  43. package/dist/extension/lib/telegram/confirmations.mjs +2 -2
  44. package/dist/extension/lib/telegram/conversations.d.ts +1 -1
  45. package/dist/extension/lib/telegram/conversations.mjs +17 -4
  46. package/dist/extension/lib/telegram/deferred-confirmations.mjs +2 -2
  47. package/dist/extension/lib/telegram/health.d.ts +1 -0
  48. package/dist/extension/lib/telegram/health.mjs +2 -1
  49. package/dist/extension/lib/telegram/inbound.d.ts +7 -0
  50. package/dist/extension/lib/telegram/inbound.mjs +47 -15
  51. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  52. package/dist/extension/lib/telegram/observed.d.ts +5 -98
  53. package/dist/extension/lib/telegram/observed.mjs +11 -92
  54. package/dist/extension/lib/telegram/router.d.ts +4 -114
  55. package/dist/extension/lib/telegram/router.mjs +4 -250
  56. package/dist/extension/lib/telegram/routing-context.d.ts +3 -3
  57. package/dist/extension/lib/telegram/routing-context.mjs +7 -86
  58. package/dist/extension/lib/telegram/signals.d.ts +5 -0
  59. package/dist/extension/lib/telegram/signals.mjs +60 -0
  60. package/dist/extension/lib/telegram/silent.mjs +1 -1
  61. package/dist/extension/tools/attachments_promote.mjs +1 -1
  62. package/dist/extension/tools/attachments_send.mjs +1 -1
  63. package/package.json +2 -2
  64. package/dist/_chunks/conversation-runtime-ovs8IhXJ.mjs +0 -212
@@ -0,0 +1,109 @@
1
+ import { type GroupPolicy } from "./policy.js";
2
+ import type { ObservedMessage, PendingConfirmation, RecentMessage } from "./observed.js";
3
+ import { type MessageAudience, type AgentIdentity as BotIdentity, type AddressingReason, type GroupMessageSignals } from "./signals.js";
4
+ import type { PendingQuestion } from "../questions.js";
5
+ export type { AddressingReason } from "./signals.js";
6
+ export type Route = "observe" | "reply";
7
+ /** What the decision model sees. */
8
+ export interface RouteQuestion {
9
+ readonly groups?: GroupPolicy;
10
+ readonly signals?: GroupMessageSignals;
11
+ readonly agentInstructions?: string;
12
+ /** The agent's own description, so the model knows what it can be asked for. */
13
+ readonly agent: string;
14
+ readonly agentIdentity?: Partial<BotIdentity> & {
15
+ readonly aliases: ReadonlyArray<string>;
16
+ };
17
+ readonly pendingQuestions?: ReadonlyArray<PendingQuestion & {
18
+ readonly at?: string;
19
+ }>;
20
+ readonly pendingConfirmations?: ReadonlyArray<PendingConfirmation>;
21
+ /** Registry purpose and usage guidance, available before deciding whether to answer. */
22
+ readonly chat?: ReadonlyArray<string>;
23
+ /** Recent received messages and delivered agent replies, oldest first. */
24
+ readonly recent: ReadonlyArray<RecentMessage>;
25
+ readonly message: ObservedMessage;
26
+ readonly audience?: MessageAudience;
27
+ /** Provider media types when the sender supplied no caption (a transcript may still exist). */
28
+ readonly captionlessAttachments?: ReadonlyArray<string>;
29
+ }
30
+ export interface RouteDecision {
31
+ readonly route: Route;
32
+ readonly reason: string;
33
+ /** Short observable routing explanation, not hidden model reasoning. */
34
+ readonly note: string;
35
+ readonly diagnostics?: RouteDiagnostics;
36
+ readonly router?: Readonly<Record<string, unknown>>;
37
+ readonly shadow?: Readonly<Record<string, unknown>>;
38
+ }
39
+ export interface RouteDiagnostics {
40
+ readonly model: string;
41
+ readonly latencyMs: number;
42
+ readonly probabilities?: Readonly<Record<Route, number>>;
43
+ }
44
+ /** Decides whether a group message not addressed to the bot is still for it. */
45
+ export type RouteDecider = (question: RouteQuestion) => Promise<Route | {
46
+ readonly route: Route;
47
+ readonly diagnostics: RouteDiagnostics;
48
+ readonly router?: Readonly<Record<string, unknown>>;
49
+ }>;
50
+ /**
51
+ * The shape of eve's `decide` this router uses. Tests pass a fake; production passes `decide`
52
+ * from `eve/ai`, which authenticates to AI Gateway like every other model call.
53
+ */
54
+ export type DecideFunction = (options: {
55
+ readonly model: string;
56
+ readonly state: {
57
+ readonly [key: string]: string | ReadonlyArray<string>;
58
+ };
59
+ readonly questions: {
60
+ readonly route: {
61
+ readonly type: "choice";
62
+ readonly instructions: string;
63
+ readonly criteria: {
64
+ readonly observe: string;
65
+ readonly reply: string;
66
+ };
67
+ };
68
+ };
69
+ readonly abortSignal?: AbortSignal;
70
+ }) => Promise<{
71
+ readonly answers: {
72
+ readonly route: {
73
+ readonly choice: Route;
74
+ readonly probabilities?: Readonly<Record<Route, number>>;
75
+ };
76
+ };
77
+ }>;
78
+ /** One decision should take a moment; a slow one gives up and observes. */
79
+ /** An unthreaded answer minutes later may continue a question, but old questions are not hints. */
80
+ export declare const FOLLOW_UP_WINDOW_MS: number;
81
+ /** Shared bounded conversation evidence for routing and the main model. Facts are hints only. */
82
+ export declare function conversationContext(question: RouteQuestion): Parameters<DecideFunction>[0]["state"];
83
+ /** A {@link RouteDecider} that asks one `decide` question on the configured decision model. */
84
+ export declare function decisionRouter(input: {
85
+ readonly decide: DecideFunction;
86
+ readonly model: string;
87
+ }): RouteDecider;
88
+ /**
89
+ * Routes one message from a paired member. A failed decision observes: staying quiet is the safe
90
+ * mistake in a group chat.
91
+ */
92
+ export declare function routeMessage(input: {
93
+ readonly groups?: GroupPolicy;
94
+ readonly metrics?: boolean;
95
+ readonly signals?: GroupMessageSignals;
96
+ readonly pendingQuestionAt?: string;
97
+ readonly chatId: string;
98
+ readonly memberId: string;
99
+ readonly policy: "smart" | "mention";
100
+ readonly addressed: AddressingReason | undefined;
101
+ /** A mention-only answer to a recent durable question, replying to this bot. */
102
+ readonly recipientAnswer?: boolean;
103
+ readonly audience?: MessageAudience;
104
+ readonly record?: (decision: RouteDecision) => void;
105
+ /** Builds the question only when a decision is needed: it reads the agent's description. */
106
+ readonly question: () => Promise<RouteQuestion>;
107
+ readonly beforeDecide?: () => Promise<boolean>;
108
+ readonly decide: RouteDecider;
109
+ }): Promise<Route>;
@@ -0,0 +1,383 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { DEFAULT_GROUP_POLICY, groupPolicy, overlaySmartPolicy } from "./policy.mjs";
8
+ import { addressingReason, otherRecipient, signalAudience } from "./signals.mjs";
9
+ import { recordRoutingMetric } from "./health.mjs";
10
+ import { humanAddressee, humanThread, questionInvitation, restoreEngagement } from "./engagement.mjs";
11
+ import { boundedRouterPayload, policyHash } from "./record.mjs";
12
+ const FOLLOW_UP_WINDOW_MS = 18e5;
13
+ function isAgentMessage(message) {
14
+ return "role" in message && message.role === "agent";
15
+ }
16
+ function confirmationProjection(pending, at) {
17
+ return {
18
+ action: pending.action.slice(0, 180),
19
+ requested_by: pending.requestedBy?.slice(0, 180) ?? null,
20
+ age_seconds: Math.floor((Date.parse(at) - Date.parse(pending.at)) / 1e3),
21
+ state: pending.state ?? "pending"
22
+ };
23
+ }
24
+ function recipientLine(recipient) {
25
+ const identity = [...recipient.id === void 0 ? [] : [`user_id ${recipient.id}`], ...recipient.username === void 0 || recipient.author === `@${recipient.username}` ? [] : [`@${recipient.username}`]];
26
+ return identity.length === 0 ? recipient.author : `${recipient.author} (${identity.join(", ")})`;
27
+ }
28
+ function line(message, bot, roster, at) {
29
+ const kind = (recipient) => {
30
+ if (bot?.id !== void 0 && recipient.id === bot.id || recipient.username !== void 0 && recipient.username.toLowerCase() === bot?.username?.toLowerCase()) return "bot";
31
+ return roster.find((entry) => recipient.id !== void 0 && recipient.id === entry.authorId || recipient.username !== void 0 && recipient.username.toLowerCase() === entry.username?.toLowerCase())?.kind ?? recipient.kind;
32
+ };
33
+ const structure = JSON.stringify({
34
+ message_id: message.messageId ?? null,
35
+ sender: {
36
+ id: (isAgentMessage(message) ? bot?.id : void 0) ?? message.authorId ?? null,
37
+ kind: isAgentMessage(message) || bot?.id !== void 0 && message.authorId === bot.id ? "bot" : message.kind ?? "unknown",
38
+ membership: message.membership ?? "unknown"
39
+ },
40
+ reply_to: message.replyTo === void 0 ? null : {
41
+ ...message.replyTo,
42
+ kind: kind(message.replyTo)
43
+ },
44
+ tagged_users: (message.taggedUsers ?? []).map((recipient) => ({
45
+ ...recipient,
46
+ kind: kind(recipient)
47
+ })),
48
+ attachment_kinds: message.attachmentKinds ?? [],
49
+ has_caption: message.hasCaption ?? false,
50
+ revision: message.revision ?? { edited: false }
51
+ });
52
+ if (isAgentMessage(message)) {
53
+ if (message.confirmation !== void 0) return JSON.stringify(confirmationProjection(message.confirmation, at));
54
+ const addressedTo = message.addressedTo.map(recipientLine).join(", ");
55
+ const replyingTo = message.inReplyTo === void 0 ? void 0 : recipientLine(message.inReplyTo);
56
+ const recipients = addressedTo === "" ? "recipient unspecified" : `addressed to ${addressedTo}`;
57
+ const reply = replyingTo === void 0 ? "" : `; responding to ${replyingTo}`;
58
+ return `[${message.at}] THE AGENT (${recipients}${reply}): ${message.text.slice(0, 1e3)}\nstructure: ${structure}`;
59
+ }
60
+ const author = recipientLine({
61
+ author: message.author,
62
+ ...message.authorId === void 0 ? {} : { id: message.authorId },
63
+ ...message.username === void 0 ? {} : { username: message.username }
64
+ });
65
+ return [
66
+ `[${message.at}] ${author}: ${message.text.slice(0, 1e3)}`,
67
+ ...message.media ? [message.media.slice(0, 4e3)] : [],
68
+ `structure: ${structure}`
69
+ ].join("\n");
70
+ }
71
+ function recentlyAddressed(message, sender) {
72
+ return message.addressedTo.some((recipient) => recipient.id !== void 0 ? recipient.id === sender.authorId : recipient.username !== void 0 && sender.username !== void 0 && recipient.username.toLowerCase() === sender.username.toLowerCase());
73
+ }
74
+ function lastOutbound(question) {
75
+ return question.recent.findLast((message) => isAgentMessage(message) && message.messageId !== void 0 && Date.parse(message.at) <= Date.parse(question.message.at));
76
+ }
77
+ function conversationContext(question) {
78
+ const sentAt = Date.parse(question.message.at);
79
+ const policy = question.groups ?? DEFAULT_GROUP_POLICY;
80
+ const recent = question.recent.slice(-policy.smart.context.messages).filter((message) => {
81
+ if (!isAgentMessage(message)) return true;
82
+ const age = sentAt - Date.parse(message.at);
83
+ return age >= 0 && age <= policy.smart.engagement.followUpMinutes * 6e4;
84
+ });
85
+ const followUps = recent.filter((message) => isAgentMessage(message) && recentlyAddressed(message, question.message));
86
+ const bot = question.agentIdentity;
87
+ const outbound = lastOutbound(question);
88
+ const roster = [...recent, question.message];
89
+ const participants = new Map();
90
+ for (const message of [...recent, question.message]) {
91
+ if (isAgentMessage(message) && message.confirmation !== void 0) continue;
92
+ const id = (isAgentMessage(message) ? bot?.id : void 0) ?? message.authorId;
93
+ participants.set(id ?? message.username ?? message.author, JSON.stringify({
94
+ id: id ?? null,
95
+ display_name: isAgentMessage(message) ? bot?.displayName ?? "the agent" : message.author,
96
+ username: message.username ?? (isAgentMessage(message) ? bot?.username : void 0) ?? null,
97
+ kind: isAgentMessage(message) || bot?.id !== void 0 && id === bot.id ? "bot" : message.kind ?? "unknown",
98
+ membership: message.membership ?? "unknown"
99
+ }));
100
+ }
101
+ const openQuestions = recent.filter(isAgentMessage).filter((message) => message.confirmation === void 0).map((message) => JSON.stringify({
102
+ source: "recent_agent_message",
103
+ status: "candidate",
104
+ message_id: message.messageId ?? null,
105
+ at: message.at,
106
+ age_seconds: Math.floor((sentAt - Date.parse(message.at)) / 1e3),
107
+ addressed_to: message.addressedTo,
108
+ in_reply_to: message.inReplyTo ?? null
109
+ }));
110
+ for (const pending of (question.pendingQuestions ?? []).slice(-5)) openQuestions.push(JSON.stringify({
111
+ source: "durable_pending_question",
112
+ status: questionInvitation(pending.at, question.message.at, policy.smart.engagement.openQuestionMinutes) ? "pending" : "stale",
113
+ id: pending.id,
114
+ question: pending.question.slice(0, 1e3),
115
+ task: pending.task.slice(0, 1e3),
116
+ member_id: pending.memberId,
117
+ at: pending.at ?? null,
118
+ age_seconds: pending.at === void 0 ? null : Math.max(0, Math.floor((sentAt - Date.parse(pending.at)) / 1e3))
119
+ }));
120
+ return {
121
+ agent: policy.smart.context.includeAgentDescription ? question.agent.slice(0, 4e3) : "",
122
+ ...policy.smart.context.includeAgentInstructions ? { agent_instructions: (question.agentInstructions ?? "").slice(0, typeof policy.smart.context.includeAgentInstructions === "object" ? policy.smart.context.includeAgentInstructions.maxChars : 6e3) } : {},
123
+ agent_identity: JSON.stringify({
124
+ display_name: bot?.displayName ?? null,
125
+ username: bot?.username ?? null,
126
+ aliases: bot?.aliases ?? []
127
+ }),
128
+ group_guidance: (policy.smart.context.includeChatGuidance ? question.chat ?? [] : []).slice(0, 5).map((line) => line.slice(0, 2e3)),
129
+ participants: policy.smart.context.includeParticipants ? [...participants.values()] : [],
130
+ ...question.signals === void 0 || policy.smart.engagement.policy === "legacy" ? {} : {
131
+ channel: JSON.stringify({
132
+ provider: question.signals.provider,
133
+ thread_with_agent: question.signals.addressing.threadWithAgent
134
+ }),
135
+ engagement: JSON.stringify(restoreEngagement(question.signals, question.recent, policy) ?? null),
136
+ addressee_analysis: JSON.stringify({
137
+ human: humanAddressee(question.signals, question.recent, policy.participants) ?? null,
138
+ human_thread: humanThread(question.signals, question.recent, policy.smart.engagement.followUpMinutes)
139
+ })
140
+ },
141
+ recent_messages: recent.map((message) => line(message, bot, roster, question.message.at)),
142
+ possible_follow_up_to: followUps.map((message) => line(message, bot, roster, question.message.at)),
143
+ open_questions: openQuestions,
144
+ last_outbound: JSON.stringify(outbound === void 0 ? null : {
145
+ ...outbound.confirmation === void 0 ? { text: outbound.text.slice(0, 1e3) } : { confirmation: confirmationProjection(outbound.confirmation, question.message.at) },
146
+ age_seconds: Math.floor((sentAt - Date.parse(outbound.at)) / 1e3),
147
+ is_question: outbound.confirmation === void 0 && /[??]/u.test(outbound.text),
148
+ is_confirmation_prompt: outbound.confirmation !== void 0
149
+ }),
150
+ pending_confirmations: (question.pendingConfirmations ?? []).slice(-5).map((pending) => JSON.stringify(confirmationProjection(pending, question.message.at))),
151
+ message: line(question.message, bot, roster, question.message.at),
152
+ original_text: (question.audience?.originalText ?? question.message.text).slice(0, 1e3),
153
+ reply_target: JSON.stringify(question.recent.some((message) => isAgentMessage(message) && message.confirmation !== void 0 && message.messageId === question.audience?.replyTo?.messageId) ? {
154
+ ...question.audience?.replyTo,
155
+ text: void 0
156
+ } : question.audience?.replyTo ?? null),
157
+ tagged_users: (question.audience?.mentions ?? []).map((item) => JSON.stringify(item)),
158
+ bot_tagged: String(question.audience?.botTagged ?? false),
159
+ conversation_signals: JSON.stringify({
160
+ reply_to: question.message.replyTo ?? null,
161
+ tagged_users: question.message.taggedUsers ?? [],
162
+ human_to_human_reply: question.message.kind === "human" && question.message.replyTo?.kind === "human",
163
+ recent_human_reply_edges: recent.filter((message) => !isAgentMessage(message) && message.replyTo?.kind === "human").slice(-5).map((message) => ({
164
+ message_id: message.messageId ?? null,
165
+ sender_id: message.authorId ?? null,
166
+ reply_to: message.replyTo
167
+ })),
168
+ seconds_since_last_outbound: outbound === void 0 ? null : Math.floor((sentAt - Date.parse(outbound.at)) / 1e3)
169
+ }),
170
+ captionless_attachments: question.captionlessAttachments ?? []
171
+ };
172
+ }
173
+ function decisionRouter(input) {
174
+ return async (question) => {
175
+ const policy = question.groups?.smart;
176
+ const started = performance.now();
177
+ const abortSignal = AbortSignal.timeout(Math.ceil((policy?.timeoutSeconds ?? 15) * 1e3));
178
+ const state = conversationContext(question);
179
+ const questions = { route: {
180
+ type: "choice",
181
+ instructions: policy?.instructions ?? DEFAULT_GROUP_POLICY.smart.instructions,
182
+ criteria: policy?.criteria ?? DEFAULT_GROUP_POLICY.smart.criteria
183
+ } };
184
+ const result = await input.decide({
185
+ model: policy?.model ?? input.model,
186
+ state,
187
+ questions,
188
+ abortSignal
189
+ }).catch((error) => {
190
+ if (abortSignal.aborted) throw new RoutingTimeoutError(error);
191
+ throw error;
192
+ });
193
+ const { choice, probabilities } = result.answers.route;
194
+ return {
195
+ route: choice,
196
+ router: {
197
+ state,
198
+ questions,
199
+ answer: result.answers.route
200
+ },
201
+ diagnostics: {
202
+ model: policy?.model ?? input.model,
203
+ latencyMs: Math.round(performance.now() - started),
204
+ ...probabilities === void 0 ? {} : { probabilities }
205
+ }
206
+ };
207
+ };
208
+ }
209
+ var RoutingTimeoutError = class extends Error {
210
+ constructor(cause) {
211
+ super("Group routing decision timed out.", { cause });
212
+ this.name = "TimeoutError";
213
+ }
214
+ };
215
+ async function routeMessage(input) {
216
+ const groups = input.groups ?? groupPolicy({}, { mode: input.policy });
217
+ const signals = input.signals;
218
+ const audience = input.audience ?? (signals === void 0 ? void 0 : signalAudience(signals));
219
+ let addressed = input.addressed ?? (signals === void 0 ? void 0 : addressingReason(signals));
220
+ if (addressed === "pending-question" && !questionInvitation(input.pendingQuestionAt, signals?.message.at ?? new Date().toISOString(), groups.smart.engagement.openQuestionMinutes)) addressed = void 0;
221
+ let payload = {
222
+ policy_hash: policyHash(groups),
223
+ provider: signals?.provider,
224
+ signals,
225
+ addressed: input.addressed,
226
+ pending_question_at: input.pendingQuestionAt,
227
+ recipient_answer: input.recipientAnswer,
228
+ policy: {
229
+ ...groups,
230
+ chats: {},
231
+ observability: { storeRouterPayload: false }
232
+ }
233
+ };
234
+ let question;
235
+ let shadow;
236
+ let shadowRun;
237
+ const evaluateShadow = () => {
238
+ if (shadowRun !== void 0) return shadowRun;
239
+ shadowRun = (async () => {
240
+ if (groups.observability.shadow === void 0) return;
241
+ try {
242
+ await routeMessage({
243
+ ...input,
244
+ groups: {
245
+ ...groups,
246
+ smart: overlaySmartPolicy(groups.observability.shadow, groups),
247
+ observability: {
248
+ ...groups.observability,
249
+ shadow: void 0
250
+ }
251
+ },
252
+ beforeDecide: void 0,
253
+ metrics: false,
254
+ question: async () => question ?? input.question(),
255
+ record: (decision) => {
256
+ shadow = { ...decision };
257
+ }
258
+ });
259
+ } catch (error) {
260
+ console.error("[segmentstream] group shadow routing failed", { error });
261
+ shadow = { reason: "shadow_failed" };
262
+ }
263
+ })();
264
+ return shadowRun;
265
+ };
266
+ const complete = async (route, reason, note, logReason = reason, diagnostics) => {
267
+ if (reason !== "admission_refused") await evaluateShadow();
268
+ payload = {
269
+ ...payload,
270
+ decision: {
271
+ route,
272
+ reason
273
+ }
274
+ };
275
+ if (input.metrics !== false) recordRoutingMetric(signals?.provider ?? "unknown", route, reason, diagnostics?.latencyMs);
276
+ input.record?.({
277
+ route,
278
+ reason,
279
+ note,
280
+ ...groups.observability.storeRouterPayload ? { router: boundedRouterPayload(payload) } : {},
281
+ ...shadow === void 0 ? {} : { shadow: groups.observability.storeRouterPayload ? boundedRouterPayload(shadow) : {
282
+ route: shadow.route,
283
+ reason: shadow.reason,
284
+ diagnostics: shadow.diagnostics
285
+ } },
286
+ ...diagnostics === void 0 ? {} : { diagnostics }
287
+ });
288
+ if (reason !== "decision_model_failed" && reason !== "decision_model_timeout") console.info("[segmentstream] telegram route", {
289
+ route,
290
+ reason: logReason,
291
+ ...diagnostics
292
+ });
293
+ return route;
294
+ };
295
+ if (addressed !== void 0 && addressed !== "pending-question" && addressed !== "reply-to-bot") return complete("reply", addressed, `Bot addressed directly: ${addressed}.`);
296
+ if (input.recipientAnswer && addressed === "reply-to-bot") return complete("reply", "pending-question", "Recipient supplied as the pending question's answer.");
297
+ const elsewhere = otherRecipient(audience);
298
+ if (elsewhere !== void 0) return complete("observe", "other_recipient", elsewhere);
299
+ if (addressed === "reply-to-bot") return complete("reply", "reply-to-bot", "Bot addressed directly: reply-to-bot.");
300
+ if (signals !== void 0 && groups.smart.engagement.addresseeGate) {
301
+ const human = humanAddressee(signals, [], groups.participants);
302
+ if (human !== void 0) {
303
+ payload = {
304
+ ...payload,
305
+ addressee_analysis: { human }
306
+ };
307
+ return complete("observe", "human_addressee", "Workspace participant alias addresses another person.");
308
+ }
309
+ }
310
+ if (addressed === "pending-question") return complete("reply", "pending-question", "Answer to this member's pending agent question, without another explicit recipient.");
311
+ if (signals?.addressing.eligible === false) return complete("observe", "channel_ineligible", "Channel signal excludes ambient messages.");
312
+ if (groups.mode === "mention") return complete("observe", "mention_policy", "Mention policy observes unaddressed messages.");
313
+ if (input.beforeDecide !== void 0 && !await input.beforeDecide()) return complete("observe", "admission_refused", "Admission refused before decoding or inference.");
314
+ try {
315
+ question = {
316
+ ...await input.question(),
317
+ ...input.groups === void 0 ? {} : { groups },
318
+ ...signals === void 0 ? {} : { signals }
319
+ };
320
+ const elsewhere = otherRecipient(question.audience);
321
+ if (elsewhere !== void 0) return complete("observe", "other_recipient", elsewhere);
322
+ const human = signals === void 0 ? void 0 : humanAddressee(signals, question.recent, groups.participants);
323
+ const thread = signals !== void 0 && humanThread(signals, question.recent, groups.smart.engagement.followUpMinutes);
324
+ const engagement = signals === void 0 ? void 0 : restoreEngagement(signals, question.recent, groups);
325
+ payload = {
326
+ ...payload,
327
+ engagement,
328
+ addressee_analysis: {
329
+ human,
330
+ human_thread: thread
331
+ }
332
+ };
333
+ if (groups.smart.engagement.addresseeGate && human !== void 0) return complete("observe", "human_addressee", "Participant name addresses another person.");
334
+ payload = {
335
+ ...payload,
336
+ question: {
337
+ recent: question.recent,
338
+ pendingQuestions: question.pendingQuestions,
339
+ pendingConfirmations: question.pendingConfirmations,
340
+ agentIdentity: question.agentIdentity,
341
+ chat: question.chat,
342
+ audience: question.audience
343
+ },
344
+ state: conversationContext(question),
345
+ questions: { route: {
346
+ type: "choice",
347
+ instructions: groups.smart.instructions,
348
+ criteria: groups.smart.criteria
349
+ } }
350
+ };
351
+ evaluateShadow();
352
+ const result = await input.decide(question);
353
+ const probability = typeof result === "string" ? void 0 : result.diagnostics.probabilities?.reply;
354
+ const threshold = thread ? groups.smart.thresholds.humanThread : engagement?.strength === "explicit" ? groups.smart.thresholds.engaged : groups.smart.thresholds.neutral;
355
+ const selected = typeof result === "string" ? result : result.route;
356
+ const below = (groups.smart.engagement.policy === "engaged" || Object.entries(groups.sources).some(([path, source]) => path.startsWith("smart.thresholds.") && source !== "platform")) && selected === "reply" && probability !== void 0 && probability < threshold;
357
+ const route = below ? "observe" : selected;
358
+ payload = {
359
+ ...payload,
360
+ ...typeof result === "string" ? {
361
+ state: conversationContext(question),
362
+ answer: { choice: result }
363
+ } : result.router,
364
+ thresholds_applied: {
365
+ threshold,
366
+ probability,
367
+ category: thread ? "humanThread" : engagement?.strength === "explicit" ? "engaged" : "neutral"
368
+ }
369
+ };
370
+ return complete(route, below ? "decision_model_below_threshold" : `decision_model_${route}`, `Smart model selected ${route}; reply target: ${question.audience?.replyTo?.kind ?? "none"}; tagged recipients: ${question.audience?.mentions.length ?? 0}.`, "decision-model", typeof result === "string" ? void 0 : result.diagnostics);
371
+ } catch (error) {
372
+ const timeout = error instanceof Error && error.name === "TimeoutError";
373
+ const note = timeout ? "Routing timed out; observe fallback." : "Routing failed; observe fallback.";
374
+ console.error("[segmentstream] telegram route", {
375
+ route: "observe",
376
+ reason: timeout ? "decision-timeout" : "decision-failure",
377
+ note,
378
+ error
379
+ });
380
+ return complete("observe", timeout ? "decision_model_timeout" : "decision_model_failed", note);
381
+ }
382
+ }
383
+ export { FOLLOW_UP_WINDOW_MS, conversationContext, decisionRouter, routeMessage };
@@ -0,0 +1,4 @@
1
+ import { type RecentMessage } from "./observed.js";
2
+ import { type ConversationPage } from "../conversations.js";
3
+ /** Cold ring: the same 30 messages, including unlinked humans and confirmed delivered bot messages. */
4
+ export declare function archivedRoutingMessages(events: ConversationPage["events"], limit?: number): ReadonlyArray<RecentMessage>;
@@ -0,0 +1,96 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { agentMessage } from "./observed.mjs";
8
+ import { collapseConversationEvents } from "../conversations.mjs";
9
+ function archivedMetadata(event) {
10
+ try {
11
+ return JSON.parse(event.argsSummary ?? "null")?.routing ?? {};
12
+ } catch {
13
+ return {};
14
+ }
15
+ }
16
+ function archivedRoutingMessages(events, limit = 30) {
17
+ const messages = collapseConversationEvents(events).filter((event) => event.kind === "inbound" || event.kind === "outbound" && (event.status === "sent" || event.status === "completed"));
18
+ const deferredStates = new Map();
19
+ for (const event of [...events].reverse()) {
20
+ if (event.kind !== "tool_result" || event.operationId == null) continue;
21
+ const state = event.reason === "done" ? "completed" : event.reason;
22
+ if ([
23
+ "approved",
24
+ "cancelled",
25
+ "expired",
26
+ "completed",
27
+ "failed"
28
+ ].includes(state ?? "")) deferredStates.set(event.operationId, state);
29
+ }
30
+ const invitations = new Map(events.filter((event) => event.kind === "decision" && event.decision === "reply" && event.messageId !== null && [
31
+ "mention",
32
+ "command",
33
+ "reply-to-bot",
34
+ "thread-with-agent",
35
+ "pending-question"
36
+ ].includes(event.reason ?? "")).map((event) => [event.messageId, event.reason]));
37
+ const membership = new Map();
38
+ for (const event of [...events].reverse()) {
39
+ if (event.kind !== "decision" || event.senderId === null) continue;
40
+ const known = archivedMetadata(event).membership;
41
+ if (known === "linked" || known === "unlinked") membership.set(event.senderId, known);
42
+ else if (event.reason === "identity_unknown") membership.set(event.senderId, "unlinked");
43
+ }
44
+ return messages.slice(0, limit).reverse().map((event) => {
45
+ const metadata = archivedMetadata(event);
46
+ const prompt = metadata.confirmation;
47
+ const decision = prompt?.mode === "deferred" ? deferredStates.get(prompt.requestId) : void 0;
48
+ const parent = messages.find((candidate) => candidate.messageId !== null && candidate.messageId === event.replyToMessageId);
49
+ const replyTo = metadata.replyTo ?? (event.replyToMessageId === null ? void 0 : {
50
+ messageId: event.replyToMessageId,
51
+ kind: parent === void 0 ? "unknown" : parent.kind === "outbound" ? "bot" : parent.senderIsBot ? "other_bot" : "human",
52
+ ...parent?.senderId == null ? {} : { id: parent.senderId },
53
+ ...parent?.senderName == null ? {} : { name: parent.senderName }
54
+ });
55
+ const derivedText = metadata.decoded?.flatMap((item) => item.content === void 0 ? [] : [item.content]).join("\n");
56
+ const text = (event.text || derivedText || (metadata.media ? "" : "[attachment or service message]")).slice(0, 2e3);
57
+ const common = {
58
+ invitation: metadata.invitation ?? invitations.get(event.messageId ?? ""),
59
+ author: event.senderName ?? event.senderId ?? "unknown sender",
60
+ text,
61
+ ...metadata.media === void 0 ? {} : { media: metadata.media },
62
+ at: event.at,
63
+ ...event.messageId === null ? {} : { messageId: event.messageId },
64
+ ...event.senderId === null ? {} : { authorId: event.senderId },
65
+ ...metadata.username === void 0 ? {} : { username: metadata.username },
66
+ kind: metadata.kind ?? (event.senderIsBot ? "other_bot" : event.senderId === null ? "unknown" : "human"),
67
+ membership: metadata.membership ?? membership.get(event.senderId ?? "") ?? "unknown",
68
+ ...replyTo === void 0 ? {} : { replyTo },
69
+ taggedUsers: metadata.taggedUsers ?? [...text.matchAll(/(?:^|[^A-Za-z0-9_])@([A-Za-z0-9_]+)/gu)].map((match) => ({
70
+ type: "mention",
71
+ kind: "unknown",
72
+ username: match[1],
73
+ text: `@${match[1]}`
74
+ })),
75
+ attachmentKinds: metadata.attachmentKinds ?? [...new Set(event.attachments.map((file) => file.kind))],
76
+ hasCaption: metadata.hasCaption ?? (event.attachments.length > 0 && (event.text?.trim() ?? "") !== ""),
77
+ revision: metadata.revision ?? { edited: event.eventId.includes(":edit:") }
78
+ };
79
+ return event.kind === "outbound" ? {
80
+ ...common,
81
+ ...prompt === void 0 ? {} : { confirmation: {
82
+ ...prompt,
83
+ ...decision === void 0 ? {} : { state: decision }
84
+ } },
85
+ ...agentMessage({
86
+ text,
87
+ at: event.at,
88
+ ...metadata.inReplyTo !== void 0 ? { inReplyTo: metadata.inReplyTo } : parent?.senderId == null ? {} : { inReplyTo: {
89
+ id: parent.senderId,
90
+ author: parent.senderName ?? parent.senderId
91
+ } }
92
+ })
93
+ } : common;
94
+ });
95
+ }
96
+ export { archivedRoutingMessages };
@@ -0,0 +1,14 @@
1
+ import { type GroupPolicyInput } from "@segmentstream-cloud/ontology/groups";
2
+ import { type Environment } from "../environment.js";
3
+ import { type RuntimeToken } from "../runtime-token.js";
4
+ export interface CloudGroupSettings {
5
+ readonly settings: GroupPolicyInput;
6
+ readonly instructions?: string;
7
+ }
8
+ /** Bounded request-scoped identity, one policy refresh per minute, last known override on transient failure. */
9
+ export declare function groupSettings(input?: {
10
+ readonly environment?: Environment;
11
+ readonly token?: RuntimeToken;
12
+ readonly fetch?: typeof fetch;
13
+ readonly now?: () => number;
14
+ }): () => Promise<CloudGroupSettings | undefined>;
@@ -0,0 +1,58 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { proxyEnvironment } from "../environment.mjs";
8
+ import { runtimeToken } from "../runtime-token.mjs";
9
+ import { groupPolicyIssues } from "@segmentstream-cloud/ontology/groups";
10
+ function groupSettings(input = {}) {
11
+ let cached;
12
+ let expires = 0;
13
+ let etag;
14
+ let pending;
15
+ const now = input.now ?? Date.now;
16
+ return async () => {
17
+ if (now() < expires) return cached;
18
+ if (pending !== void 0) return pending;
19
+ pending = (async () => {
20
+ try {
21
+ const env = proxyEnvironment(input.environment);
22
+ const identity = await (input.token ?? runtimeToken)();
23
+ const workspacePath = `/api/internal/v1/workspaces/${encodeURIComponent(env.workspaceId)}`;
24
+ const response = await (input.fetch ?? fetch)(`${env.segmentstreamUrl}${workspacePath}/agent/groups/effective`, {
25
+ redirect: "error",
26
+ headers: {
27
+ "x-segmentstream-organization-id": env.organizationId,
28
+ "x-segmentstream-runtime-identity": identity,
29
+ ...etag === void 0 ? {} : { "if-none-match": etag }
30
+ },
31
+ signal: AbortSignal.timeout(5e3)
32
+ });
33
+ if (response.status === 304) return cached;
34
+ if (response.status === 404) return cached;
35
+ if (!response.ok) throw new Error(`Group settings read failed (${response.status}); request ${response.headers.get("x-request-id") ?? "unknown"}.`);
36
+ const value = await response.json();
37
+ if (value.settings === void 0 || groupPolicyIssues(value.settings).length > 0) throw new Error("Invalid group settings response.");
38
+ cached = {
39
+ settings: value.settings,
40
+ ...typeof value.instructions === "string" ? { instructions: value.instructions.slice(0, 6e3) } : {}
41
+ };
42
+ etag = response.headers.get("etag") ?? (typeof value.version === "string" ? `"${value.version}"` : void 0);
43
+ return cached;
44
+ } catch (error) {
45
+ console.error("[segmentstream] group settings refresh failed", { error });
46
+ return cached;
47
+ } finally {
48
+ expires = now() + 6e4;
49
+ }
50
+ })();
51
+ try {
52
+ return await pending;
53
+ } finally {
54
+ pending = void 0;
55
+ }
56
+ };
57
+ }
58
+ export { groupSettings };