@segmentstream/agent 0.8.24 → 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-BrXE_Dxu.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
@@ -4,7 +4,7 @@ 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
- var version = "0.8.24";
7
+ var version = "0.9.0";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -0,0 +1,102 @@
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 CONTENT_CAPS = {
8
+ args: 16384,
9
+ result: 32768,
10
+ error: 4096
11
+ };
12
+ const sensitive = /token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn|email|phone|address|birth|document.?text|raw.?content|body|bytes|base64/i;
13
+ const encoder = new TextEncoder();
14
+ function luhn(value) {
15
+ const digits = value.replace(/\D/g, "");
16
+ if (digits.length < 13 || digits.length > 19) return false;
17
+ let sum = 0;
18
+ let double = false;
19
+ for (let i = digits.length - 1; i >= 0; i--) {
20
+ let n = Number(digits[i]);
21
+ if (double && (n *= 2) > 9) n -= 9;
22
+ sum += n;
23
+ double = !double;
24
+ }
25
+ return sum % 10 === 0;
26
+ }
27
+ function redactText(value) {
28
+ return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "[binary data]").replace(/-----BEGIN [^-\r\n]+-----[\s\S]*?(?:-----END [^-\r\n]+-----|$)/g, "[redacted PEM]").replace(/data:[^\s"']+/gi, "[binary data]").replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9+/_=.:~\-]+/gi, "[redacted authorization]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted JWT]").replace(/\b(?:ssw_|sscli_|sk[-_]|rk[-_])[A-Za-z0-9._~\-]+/gi, "[redacted key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@").replace(/([?&](?:[^=&#\s]*(?:token|password|secret|key|signature|credential|session|auth|cookie)[^=&#\s]*|sig)=)[^&#\s"']*/gi, "$1[redacted]").replace(/((?:["']?)(?:[\w-]*(?:token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn)[\w-]*)(?:["']?)\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;})]+)/gi, "$1[redacted]").replace(/\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]){11,30}\b/gi, "[redacted IBAN]").replace(/\b\d(?:[ -]?\d){12,18}\b/g, (candidate) => luhn(candidate) ? "[redacted card]" : candidate).replace(/[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9.-]*[A-Z0-9])?\.[A-Z]{2,}/gi, "[email]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[redacted SSN]").replace(/\b[A-Za-z0-9+/]{128,}={0,2}/g, "[binary data]");
29
+ }
30
+ const documentField = /^(?:text|content|body|csv|tsv|file[_-]?body|email[_-]?body)$/i;
31
+ const omittedDocument = "[omitted: unstructured document content]";
32
+ function redactContent(value) {
33
+ const seen = new WeakSet();
34
+ let nodes = 0;
35
+ const visit = (item, depth, field) => {
36
+ if (++nodes > 5e3 || depth > 12) return "[omitted: content complexity]";
37
+ if (typeof item === "string") {
38
+ try {
39
+ const embedded = JSON.parse(item);
40
+ if (embedded !== null && typeof embedded === "object") return JSON.stringify(visit(embedded, depth + 1, field));
41
+ } catch {}
42
+ if (documentField.test(field ?? "")) return omittedDocument;
43
+ return redactText(item);
44
+ }
45
+ if (typeof item === "bigint") return item.toString();
46
+ if (item === null || typeof item !== "object") return item;
47
+ if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer) return "[binary data]";
48
+ if (seen.has(item)) return "[omitted: circular]";
49
+ seen.add(item);
50
+ if (Array.isArray(item)) {
51
+ const entries = item.slice(0, 200).map((entry) => visit(entry, depth + 1, field));
52
+ if (item.length > 200) entries.push("[omitted: array entries]");
53
+ return entries;
54
+ }
55
+ const entries = Object.entries(item);
56
+ return Object.fromEntries([...entries.slice(0, 200).map(([key, entry]) => [redactText(key), sensitive.test(key) && !documentField.test(key) ? "[redacted]" : visit(entry, depth + 1, key)]), ...entries.length > 200 ? [["_omitted", "[omitted: object entries]"]] : []]);
57
+ };
58
+ return visit(value, 0);
59
+ }
60
+ function contentText(value, cap) {
61
+ if (value == null) return null;
62
+ let parsed = value;
63
+ if (typeof value === "string") try {
64
+ parsed = JSON.parse(value);
65
+ } catch {}
66
+ const redacted = redactContent(parsed);
67
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted) ?? "[omitted]";
68
+ if (encoder.encode(text).length <= cap) return text;
69
+ let preview = text;
70
+ while (true) {
71
+ const result = JSON.stringify({
72
+ truncated: true,
73
+ preview
74
+ });
75
+ const bytes = encoder.encode(result).length;
76
+ if (bytes <= cap) return result;
77
+ const available = Math.max(0, encoder.encode(preview).length - (bytes - cap) - 4);
78
+ preview = new TextDecoder().decode(encoder.encode(preview).slice(0, available), { stream: true });
79
+ }
80
+ }
81
+ function redactRouterPayload(value) {
82
+ const seen = new WeakSet();
83
+ let nodes = 0;
84
+ const visit = (item, depth) => {
85
+ if (++nodes > 5e3 || depth > 16) return "[omitted: routing complexity]";
86
+ if (typeof item === "string") {
87
+ try {
88
+ const embedded = JSON.parse(item);
89
+ if (embedded !== null && typeof embedded === "object") return JSON.stringify(visit(embedded, depth + 1));
90
+ } catch {}
91
+ return redactText(item);
92
+ }
93
+ if (item === null || typeof item !== "object") return item;
94
+ if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer) return "[binary data]";
95
+ if (seen.has(item)) return "[omitted: circular]";
96
+ seen.add(item);
97
+ if (Array.isArray(item)) return item.slice(0, 200).map((entry) => visit(entry, depth + 1));
98
+ return Object.fromEntries(Object.entries(item).slice(0, 200).map(([key, entry]) => [key, /token|password|secret|authorization|cookie|credential|api.?key|bytes|base64/i.test(key) ? "[redacted]" : visit(entry, depth + 1)]));
99
+ };
100
+ return visit(value, 0);
101
+ }
102
+ export { contentText as n, redactRouterPayload as r, CONTENT_CAPS as t };
@@ -9,6 +9,7 @@ import { segmentstreamTelegram } from "../lib/telegram/adapter.mjs";
9
9
  import { decide } from "eve/ai";
10
10
  var telegram_default = segmentstreamTelegram({
11
11
  settings: () => segmentstream.config.channels.telegram,
12
+ groups: () => segmentstream.config.groups,
12
13
  models: () => segmentstream.config.models,
13
14
  confirmationMode: () => segmentstream.config.confirmations.mode,
14
15
  sessions: () => segmentstream.config.sessions,
@@ -1,3 +1,5 @@
1
+ import { type GroupPolicyInput } from "@segmentstream-cloud/ontology/groups";
2
+ import { type GroupPolicy } from "./lib/group-routing/policy.js";
1
3
  import type { AttachmentLifecycle } from "./lib/telegram/lifecycle.js";
2
4
  import { type TelegramLinkPreviews } from "@segmentstream-cloud/ontology/telegram";
3
5
  export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
@@ -51,6 +53,7 @@ export interface AdmissionSettings {
51
53
  * the values validation allows.
52
54
  */
53
55
  export interface SegmentStreamAgentConfigInput {
56
+ readonly groups?: GroupPolicyInput;
54
57
  readonly firstContact?: FirstContactSettings;
55
58
  readonly limits?: AdmissionLimitsInput;
56
59
  readonly replies?: AdmissionReplies;
@@ -58,8 +61,13 @@ export interface SegmentStreamAgentConfigInput {
58
61
  readonly telegram?: {
59
62
  /** The Connect key whose credential is the bot token, such as `telegram`. */
60
63
  readonly connection: string;
61
- /** `smart` (default) or `mention`. */
64
+ /** @deprecated Use groups.mode; conflicting values are rejected. */
62
65
  readonly policy?: string;
66
+ readonly groups?: {
67
+ readonly replyEdgeIsAddress?: boolean;
68
+ readonly topicScope?: boolean;
69
+ readonly commandsAddress?: boolean;
70
+ };
63
71
  /** External links only (default), Telegram's native first link, or no previews. */
64
72
  readonly linkPreviews?: string;
65
73
  /** Additional exact HTTP(S) origins belonging to this workspace. */
@@ -86,7 +94,7 @@ export interface SegmentStreamAgentConfigInput {
86
94
  readonly imageDescription?: string | boolean;
87
95
  /** @deprecated Compatibility alias for imageDescription; the canonical key takes precedence. */
88
96
  readonly attachments?: string | boolean;
89
- /** The Gateway decision model of the inbound router. */
97
+ /** @deprecated Use groups.smart.model; conflicting values are rejected. */
90
98
  readonly decision?: string;
91
99
  };
92
100
  readonly ontology?: {
@@ -115,6 +123,11 @@ export interface SegmentStreamAgentConfigInput {
115
123
  };
116
124
  }
117
125
  export interface TelegramSettings {
126
+ readonly groups?: {
127
+ readonly replyEdgeIsAddress?: boolean;
128
+ readonly topicScope?: boolean;
129
+ readonly commandsAddress?: boolean;
130
+ };
118
131
  readonly connection: string;
119
132
  readonly policy: TelegramPolicy;
120
133
  readonly linkPreviews?: TelegramLinkPreviews;
@@ -146,6 +159,7 @@ export interface SessionSettings {
146
159
  }
147
160
  /** The validated configuration with defaults applied. It stays JSON-serializable. */
148
161
  export interface SegmentStreamAgentConfig extends AdmissionSettings {
162
+ readonly groups: GroupPolicy;
149
163
  readonly channels: {
150
164
  readonly telegram: TelegramSettings | null;
151
165
  };
@@ -4,6 +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 { effectiveGroupPolicy, groupPolicy } from "./lib/group-routing/policy.mjs";
8
+ import { readFileSync, realpathSync } from "node:fs";
9
+ import { isAbsolute, relative, resolve } from "node:path";
10
+ import { expandGroupFiles, groupPolicyIssues } from "@segmentstream-cloud/ontology/groups";
7
11
  import { TELEGRAM_LINK_PREVIEWS, isHttpOrigin } from "@segmentstream-cloud/ontology/telegram";
8
12
  const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
9
13
  const TELEGRAM_POLICIES = ["mention", "smart"];
@@ -65,6 +69,7 @@ function telegramSettings(value, issues) {
65
69
  allowed: [
66
70
  "connection",
67
71
  "policy",
72
+ "groups",
68
73
  "linkPreviews",
69
74
  "additionalOwnOrigins",
70
75
  "attachmentLifecycle"
@@ -72,6 +77,24 @@ function telegramSettings(value, issues) {
72
77
  issues
73
78
  });
74
79
  if (telegram === void 0) return null;
80
+ const signalSettings = section({
81
+ value: telegram.groups,
82
+ path: [...path, "groups"],
83
+ allowed: [
84
+ "replyEdgeIsAddress",
85
+ "topicScope",
86
+ "commandsAddress"
87
+ ],
88
+ issues
89
+ });
90
+ for (const [key, value] of Object.entries(signalSettings ?? {})) if (typeof value !== "boolean") issues.push({
91
+ message: "must be a boolean",
92
+ path: [
93
+ ...path,
94
+ "groups",
95
+ key
96
+ ]
97
+ });
75
98
  const connection = telegram.connection;
76
99
  const policy = telegram.policy ?? "smart";
77
100
  if (typeof connection !== "string" || !CONNECTION_KEY.test(connection)) issues.push({
@@ -117,6 +140,7 @@ function telegramSettings(value, issues) {
117
140
  return typeof connection === "string" && isTelegramPolicy(policy) ? {
118
141
  connection,
119
142
  policy,
143
+ ...signalSettings === void 0 ? {} : { groups: signalSettings },
120
144
  linkPreviews,
121
145
  additionalOwnOrigins,
122
146
  ...lifecycle === void 0 ? {} : { attachmentLifecycle: {
@@ -345,6 +369,7 @@ function validateConfig(input) {
345
369
  path: [],
346
370
  allowed: [
347
371
  "channels",
372
+ "groups",
348
373
  "models",
349
374
  "ontology",
350
375
  "sessions",
@@ -390,6 +415,66 @@ function validateConfig(input) {
390
415
  path: ["confirmations", "mode"]
391
416
  });
392
417
  const telegram = telegramSettings(channels?.telegram, issues);
418
+ for (const issue of groupPolicyIssues(root?.groups)) issues.push({
419
+ ...issue,
420
+ path: ["groups", ...issue.path]
421
+ });
422
+ let authoredGroups = root?.groups;
423
+ if (authoredGroups !== void 0 && groupPolicyIssues(authoredGroups).length === 0) try {
424
+ authoredGroups = expandGroupFiles(authoredGroups, (path) => {
425
+ const root = realpathSync(process.cwd());
426
+ const file = realpathSync(resolve(root, path));
427
+ const local = relative(root, file);
428
+ if (local.startsWith("..") || isAbsolute(local)) throw new Error("Group instruction file is outside agent/.");
429
+ return readFileSync(file, "utf8");
430
+ });
431
+ } catch (cause) {
432
+ issues.push({
433
+ path: [
434
+ "groups",
435
+ "smart",
436
+ "instructions"
437
+ ],
438
+ message: cause instanceof Error ? cause.message : "instruction file could not be read"
439
+ });
440
+ }
441
+ const legacyMode = isRecord(channels?.telegram) ? channels.telegram.policy : void 0;
442
+ if (authoredGroups?.mode !== void 0 && legacyMode !== void 0 && authoredGroups.mode !== legacyMode) issues.push({
443
+ path: ["groups", "mode"],
444
+ message: "conflicts with channels.telegram.policy"
445
+ });
446
+ if (authoredGroups?.smart?.model !== void 0 && models?.decision !== void 0 && authoredGroups.smart.model !== models.decision) issues.push({
447
+ path: [
448
+ "groups",
449
+ "smart",
450
+ "model"
451
+ ],
452
+ message: "conflicts with models.decision"
453
+ });
454
+ let groups = groupPolicy();
455
+ try {
456
+ groups = groupPolicy(issues.length === 0 ? authoredGroups : void 0, {
457
+ mode: typeof legacyMode === "string" ? legacyMode : void 0,
458
+ model: typeof models?.decision === "string" ? models.decision : void 0
459
+ });
460
+ } catch (cause) {
461
+ issues.push({
462
+ path: ["groups"],
463
+ message: cause instanceof Error ? cause.message : "invalid inherited group policy"
464
+ });
465
+ }
466
+ if (issues.length === 0) for (const key of Object.keys(groups.chats)) try {
467
+ effectiveGroupPolicy(groups, key);
468
+ } catch (cause) {
469
+ issues.push({
470
+ path: [
471
+ "groups",
472
+ "chats",
473
+ key
474
+ ],
475
+ message: cause instanceof Error ? cause.message : "invalid inherited group policy"
476
+ });
477
+ }
393
478
  const chat = chatModelChoice(models?.chat, issues);
394
479
  const stt = models?.stt === false ? false : modelId({
395
480
  value: models?.stt,
@@ -397,7 +482,7 @@ function validateConfig(input) {
397
482
  path: ["models", "stt"],
398
483
  issues
399
484
  });
400
- const decision = modelId({
485
+ modelId({
401
486
  value: models?.decision,
402
487
  fallback: DEFAULT_DECISION_MODEL,
403
488
  path: ["models", "decision"],
@@ -426,11 +511,15 @@ function validateConfig(input) {
426
511
  });
427
512
  if (issues.length > 0) return { issues };
428
513
  return { value: {
429
- channels: { telegram },
514
+ groups,
515
+ channels: { telegram: telegram === null ? null : {
516
+ ...telegram,
517
+ policy: groups.mode
518
+ } },
430
519
  models: {
431
520
  chat,
432
521
  stt,
433
- decision,
522
+ decision: groups.smart.model,
434
523
  imageDescription,
435
524
  attachments
436
525
  },
@@ -17,3 +17,10 @@ export declare const segmentstream: import("eve/extension").ExtensionHandle<{
17
17
  export default segmentstream;
18
18
  export { DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, DEFAULT_ADMISSION_LIMITS, SegmentStreamAgentConfigError, manifestOf, type ChatModelChoice, type AdmissionLimits, type AdmissionLimitsInput, type AdmissionSettings, type AdmissionReplies, type FirstContactSettings, type LocalizedText, type SegmentStreamAgentConfig, type SegmentStreamAgentConfigInput, type SessionScope, type SessionSettings, type TelegramPolicy, type TelegramSettings, } from "./config.js";
19
19
  export { chooseModel, model, type DynamicModel, type ModelChoice } from "./model.js";
20
+ export { groupPolicy, effectiveGroupPolicy, DEFAULT_GROUP_POLICY, type GroupPolicy, type GroupPolicyInput, type SmartPolicyInput, type ChatOverrideInput, } from "./lib/group-routing/policy.js";
21
+ export { addressingReason as groupAddressingReason, conversationKey, type GroupMessageSignals, type AgentMessageSignals, type GroupChannelAdapter, } from "./lib/group-routing/signals.js";
22
+ export { routeMessage as routeGroupMessage, decisionRouter as groupDecisionRouter, } from "./lib/group-routing/router.js";
23
+ export { groupSettings as readGroupSettings, type CloudGroupSettings, } from "./lib/group-routing/settings.js";
24
+ export { observedMessages as groupObservedMessages, agentMessage as groupAgentMessage, type RecentMessage, type AgentMessage, type ObservedMessage, } from "./lib/group-routing/observed.js";
25
+ export { archivedRoutingMessages } from "./lib/group-routing/routing-context.js";
26
+ export { replayRouterPayload } from "./lib/group-routing/replay.js";
@@ -4,12 +4,19 @@ 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 { DEFAULT_GROUP_POLICY, effectiveGroupPolicy, groupPolicy } from "./lib/group-routing/policy.mjs";
7
8
  import { DEFAULT_ADMISSION_LIMITS, DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, configSchema, manifestOf } from "./config.mjs";
8
9
  import { chooseModel, model } from "./model.mjs";
10
+ import { addressingReason, conversationKey } from "./lib/group-routing/signals.mjs";
11
+ import { decisionRouter, routeMessage } from "./lib/group-routing/router.mjs";
12
+ import { groupSettings } from "./lib/group-routing/settings.mjs";
13
+ import { agentMessage, observedMessages } from "./lib/group-routing/observed.mjs";
14
+ import { archivedRoutingMessages } from "./lib/group-routing/routing-context.mjs";
15
+ import { replayRouterPayload } from "./lib/group-routing/replay.mjs";
9
16
  import { defineExtension } from "eve/extension";
10
17
  const extension = defineExtension({ config: configSchema });
11
18
  const segmentstream = Object.assign(extension, {
12
19
  model: (choice) => model(choice ?? (() => extension.config.models.chat)),
13
20
  manifestOf
14
21
  });
15
- export { DEFAULT_ADMISSION_LIMITS, DEFAULT_DECISION_MODEL, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, chooseModel, segmentstream as default, segmentstream, manifestOf, model };
22
+ export { DEFAULT_ADMISSION_LIMITS, DEFAULT_DECISION_MODEL, DEFAULT_GROUP_POLICY, DEFAULT_STT_MODEL, SegmentStreamAgentConfigError, archivedRoutingMessages, chooseModel, conversationKey, segmentstream as default, segmentstream, effectiveGroupPolicy, addressingReason as groupAddressingReason, agentMessage as groupAgentMessage, decisionRouter as groupDecisionRouter, observedMessages as groupObservedMessages, groupPolicy, manifestOf, model, groupSettings as readGroupSettings, replayRouterPayload, routeMessage as routeGroupMessage };
@@ -4,7 +4,7 @@ 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 { n as conversationRecorder } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
7
+ import { conversationRecorder } from "../lib/conversation-runtime.mjs";
8
8
  import { defineHook } from "eve/hooks";
9
9
  var conversations_default = defineHook({ events: { "*": conversationRecorder() } });
10
10
  export { conversations_default as default };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversations, storedConversationContext } from "../lib/conversations.mjs";
8
- import { t as conversationCoordinates } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
8
+ import { conversationCoordinates } from "../lib/conversation-runtime.mjs";
9
9
  import { defineDynamic, defineInstructions } from "eve/instructions";
10
10
  const archive = conversations();
11
11
  var conversations_default = defineDynamic({ events: { "turn.started": async (_event, context) => {
@@ -4,5 +4,5 @@ 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 { t as agentBuild } from "../../_chunks/build-metadata-BrXE_Dxu.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-DuHeGf1m.mjs";
8
8
  export { agentBuild };
@@ -4,5 +4,136 @@ 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 { n as conversationRecorder, t as conversationCoordinates } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
7
+ import { memberIdOf } from "./telegram/principal.mjs";
8
+ import { n as contentText, t as CONTENT_CAPS } from "../../_chunks/content-CvFsrmSD.mjs";
9
+ import { conversationEvent, conversations, summarizeArguments } from "./conversations.mjs";
10
+ import { parseAddress } from "./telegram/sessions.mjs";
11
+ import { completionKind } from "./telegram/silent.mjs";
12
+ function conversationCoordinates(context) {
13
+ const principal = context.session.auth.current;
14
+ const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
15
+ const address = context.channel.continuationToken;
16
+ const attributes = principal?.attributes;
17
+ const fields = address?.split(":");
18
+ const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
19
+ const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
20
+ const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
21
+ const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
22
+ if (!chatId) return;
23
+ const memberId = memberIdOf(principal);
24
+ const chatType = attributes?.chatType;
25
+ return {
26
+ provider: telegram ? "telegram" : context.channel.kind ?? "eve",
27
+ chatId,
28
+ chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
29
+ threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
30
+ participantIds: memberId === void 0 ? [] : [memberId],
31
+ senderId: principal?.subject ?? null
32
+ };
33
+ }
34
+ function conversationRecorder(archive = conversations()) {
35
+ return async (event, context) => {
36
+ const coordinates = conversationCoordinates(context);
37
+ if (coordinates === void 0) return;
38
+ const base = {
39
+ ...coordinates,
40
+ sessionId: context.session.id,
41
+ turnId: event.data !== void 0 && "turnId" in event.data ? event.data.turnId : context.session.turn.id,
42
+ at: event.meta.at
43
+ };
44
+ const eventId = `eve:${context.session.id}:${event.meta.id}`;
45
+ if (event.type === "actions.requested") for (const action of event.data.actions) {
46
+ const name = "toolName" in action ? action.toolName : action.name;
47
+ await archive.append(conversationEvent({
48
+ ...base,
49
+ eventId: `${eventId}:${action.callId}`,
50
+ kind: "tool_call",
51
+ toolCallId: action.callId,
52
+ toolName: name,
53
+ argsSummary: summarizeArguments(action.input),
54
+ argsText: contentText(action.input, CONTENT_CAPS.args),
55
+ status: "requested"
56
+ }));
57
+ }
58
+ else if (event.type === "action.result") {
59
+ const result = event.data.result;
60
+ const name = "toolName" in result ? result.toolName : "name" in result ? result.name : result.subagentName;
61
+ await archive.append(conversationEvent({
62
+ ...base,
63
+ eventId,
64
+ kind: "tool_result",
65
+ toolCallId: result.callId,
66
+ toolName: name,
67
+ status: event.data.status,
68
+ resultText: contentText("output" in result ? result.output : null, CONTENT_CAPS.result),
69
+ errorText: contentText(event.data.error?.message ?? (event.data.status === "failed" ? resultErrorMessage(result.output) : null), CONTENT_CAPS.error)
70
+ }));
71
+ } else if (event.type === "task.started" || event.type === "task.settled") await archive.append(conversationEvent({
72
+ ...base,
73
+ eventId,
74
+ kind: event.type === "task.started" ? "tool_call" : "tool_result",
75
+ toolCallId: event.data.callId,
76
+ toolName: event.data.name ?? "task",
77
+ argsSummary: "{}",
78
+ ...event.type === "task.settled" ? {
79
+ resultText: contentText(event.data.output, CONTENT_CAPS.result),
80
+ errorText: contentText(event.data.error?.message, CONTENT_CAPS.error)
81
+ } : {},
82
+ status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
83
+ }));
84
+ else if (event.type === "turn.failed" || event.type === "step.failed") await archive.append(conversationEvent({
85
+ ...base,
86
+ turnId: event.data.turnId,
87
+ eventId,
88
+ kind: "decision",
89
+ decision: "silent",
90
+ reason: event.type,
91
+ status: "failed",
92
+ errorText: contentText(event.data.message, CONTENT_CAPS.error)
93
+ }));
94
+ else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
95
+ const kind = completionKind(event.data.message, event.data.finishReason);
96
+ const silent = kind !== "reply";
97
+ const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
98
+ await archive.append(conversationEvent({
99
+ ...base,
100
+ eventId,
101
+ kind: "decision",
102
+ decision: silent ? "silent" : "reply",
103
+ reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
104
+ text: note,
105
+ argsSummary: JSON.stringify({
106
+ outputKind: kind,
107
+ outputCharacters: event.data.message?.length ?? 0,
108
+ finishReason: event.data.finishReason
109
+ })
110
+ }));
111
+ if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
112
+ ...base,
113
+ eventId: `${eventId}:outbound`,
114
+ kind: "outbound",
115
+ text: event.data.message,
116
+ senderId: "agent",
117
+ senderIsBot: true,
118
+ status: "completed"
119
+ }));
120
+ } else if (event.type === "message.received" && coordinates.provider !== "telegram") await archive.append(conversationEvent({
121
+ ...base,
122
+ eventId,
123
+ kind: "inbound",
124
+ text: event.data.message,
125
+ attachments: (event.data.parts ?? []).filter((part) => part.type === "file").map((part, index) => ({
126
+ id: `${eventId}:file:${index}`,
127
+ kind: "file",
128
+ ...part.filename ? { filename: part.filename } : {},
129
+ contentType: part.mediaType
130
+ }))
131
+ }));
132
+ };
133
+ }
134
+ function resultErrorMessage(output) {
135
+ if (typeof output === "string") return output;
136
+ if (output !== null && typeof output === "object" && "message" in output && typeof output.message === "string") return output.message;
137
+ return null;
138
+ }
8
139
  export { conversationCoordinates, conversationRecorder };
@@ -6,11 +6,11 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin } from "./environment.mjs";
8
8
  import { memberIdOf } from "./telegram/principal.mjs";
9
+ import { runtimeToken } from "./runtime-token.mjs";
9
10
  import { redactDiagnostic } from "./diagnostics.mjs";
10
11
  import { humanText, object } from "./confirmation-description.mjs";
11
12
  import { chatPostDigest, chatPostOperationId, isChatPost } from "./chat-post-confirmation.mjs";
12
13
  import { actionApproval, inputHash } from "./confirmations.mjs";
13
- import { runtimeToken } from "./runtime-token.mjs";
14
14
  import { ontologyTools } from "./ontology-tools.mjs";
15
15
  import { createHash } from "node:crypto";
16
16
  const deferredKey = (id) => createHash("sha256").update(id).digest("hex").slice(0, 20);
@@ -0,0 +1,18 @@
1
+ import type { GroupPolicy, Participants } from "./policy.js";
2
+ import type { GroupMessageSignals } from "./signals.js";
3
+ import type { RecentMessage } from "./observed.js";
4
+ export interface Engagement {
5
+ readonly memberId: string;
6
+ readonly since: string;
7
+ readonly lastAgentReplyAt: string;
8
+ readonly anchorMessageId?: string;
9
+ readonly source: string;
10
+ readonly strength: "explicit" | "model";
11
+ }
12
+ /** Compare workspace-provided names, never infer translations or personal aliases. */
13
+ export declare function humanAddressee(signals: GroupMessageSignals, recent: ReadonlyArray<RecentMessage>, participants: Participants): string | undefined;
14
+ /** A participant's own human reply edge followed by that human's response is a human thread. */
15
+ export declare function humanThread(signals: GroupMessageSignals, recent: ReadonlyArray<RecentMessage>, minutes: number): boolean;
16
+ /** Recover from delivered replies and their inbound anchors; model replies remain weak. */
17
+ export declare function restoreEngagement(signals: GroupMessageSignals, recent: ReadonlyArray<RecentMessage>, policy: GroupPolicy): Engagement | undefined;
18
+ export declare function questionInvitation(at: string | undefined, now: string, minutes: number | "unlimited"): boolean;
@@ -0,0 +1,65 @@
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 normalize = (text) => text.normalize("NFKC").toLocaleLowerCase().replace(/^@/, "");
9
+ function humanAddressee(signals, recent, participants) {
10
+ const first = /^[\s«"“'([]*([\p{L}\p{N}_-]{3,})(?=[\s,!:;?.»"”')\]]|$)/u.exec(signals.message.text)?.[1];
11
+ if (first === void 0) return;
12
+ const name = normalize(first);
13
+ for (const [id, participant] of Object.entries(participants)) {
14
+ if (id === signals.sender.externalId || id === signals.sender.member?.id || normalize(id) === normalize(signals.sender.username ?? "")) continue;
15
+ if ([id, ...participant.aliases].some((alias) => normalize(alias) === name)) return id;
16
+ }
17
+ for (const member of recent) {
18
+ if (own(member) || member.kind === "bot" || member.kind === "other_bot" || member.authorId === signals.sender.externalId) continue;
19
+ if ([member.username, member.author.split(/\s+/u)[0]].some((alias) => alias !== void 0 && normalize(alias) === name)) return member.authorId ?? member.username ?? member.author;
20
+ }
21
+ }
22
+ function humanThread(signals, recent, minutes) {
23
+ const now = Date.parse(signals.message.at);
24
+ const edge = recent.filter((m) => now >= Date.parse(m.at) && now - Date.parse(m.at) <= minutes * 6e4).findLast((m) => !own(m) && m.authorId === signals.sender.externalId && (m.replyTo?.kind === "human" || m.invitation !== void 0));
25
+ return edge?.replyTo?.kind === "human" && edge.replyTo.id !== void 0 && edge.replyTo.id !== signals.sender.externalId;
26
+ }
27
+ function restoreEngagement(signals, recent, policy) {
28
+ let state;
29
+ for (let i = 0; i < recent.length; i++) {
30
+ const message = recent[i];
31
+ if (!own(message)) {
32
+ if (message.authorId === signals.sender.externalId && (message.replyTo?.kind === "human" && message.replyTo.id !== message.authorId || message.taggedUsers?.some((target) => target.kind !== "bot") || humanAddressee({
33
+ ...signals,
34
+ message: {
35
+ ...signals.message,
36
+ text: message.text
37
+ }
38
+ }, recent, policy.participants) !== void 0)) state = void 0;
39
+ continue;
40
+ }
41
+ if (message.confirmation !== void 0 || !message.addressedTo.some((target) => target.id === signals.sender.externalId || target.username !== void 0 && normalize(target.username) === normalize(signals.sender.username ?? ""))) continue;
42
+ const anchor = recent.slice(0, i).findLast((entry) => !own(entry) && entry.authorId === signals.sender.externalId && (message.replyTo === void 0 || entry.messageId === message.replyTo.messageId));
43
+ const invitation = message.invitation ?? anchor?.invitation ?? (anchor?.replyTo?.kind === "bot" ? "reply-to-bot" : anchor?.taggedUsers?.some((target) => target.kind === "bot") ? "mention" : void 0);
44
+ const continuous = state?.strength === "explicit" && Date.parse(message.at) - Date.parse(state.lastAgentReplyAt) <= policy.smart.engagement.conversationMinutes * 6e4;
45
+ state = invitation === void 0 && continuous ? {
46
+ ...state,
47
+ lastAgentReplyAt: message.at
48
+ } : {
49
+ memberId: signals.sender.member?.id ?? signals.sender.externalId,
50
+ since: anchor?.at ?? message.at,
51
+ lastAgentReplyAt: message.at,
52
+ anchorMessageId: anchor?.messageId,
53
+ source: invitation ?? "model",
54
+ strength: invitation === void 0 ? "model" : "explicit"
55
+ };
56
+ }
57
+ const age = Date.parse(signals.message.at) - Date.parse(state?.lastAgentReplyAt ?? "");
58
+ return state !== void 0 && age >= 0 && age <= policy.smart.engagement.conversationMinutes * 6e4 ? state : void 0;
59
+ }
60
+ function questionInvitation(at, now, minutes) {
61
+ if (minutes === "unlimited") return true;
62
+ const age = Date.parse(now) - Date.parse(at ?? "");
63
+ return minutes > 0 && age >= 0 && age <= minutes * 6e4;
64
+ }
65
+ export { humanAddressee, humanThread, questionInvitation, restoreEngagement };
@@ -0,0 +1,11 @@
1
+ import type { Route } from "./router.js";
2
+ /** Aggregate facts only: no chat, member or message identifiers enter process health. */
3
+ export declare function recordRoutingMetric(provider: string, route: Route, reason: string, latencyMs?: number): void;
4
+ export declare function groupRoutingHealth(): {
5
+ provider: string;
6
+ route: Route;
7
+ reason: string;
8
+ count: number;
9
+ modelCalls: number;
10
+ averageLatencyMs: number | null;
11
+ }[];