@segmentstream/agent 0.27.0 → 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 (28) hide show
  1. package/dist/_chunks/{build-metadata-R_ssK2uR.mjs → build-metadata-CMmCRD8l.mjs} +1 -1
  2. package/dist/extension/config.mjs +7 -1
  3. package/dist/extension/extension.d.ts +1 -1
  4. package/dist/extension/extension.mjs +2 -2
  5. package/dist/extension/lib/build-metadata.mjs +1 -1
  6. package/dist/extension/lib/conversation-runtime.d.ts +1 -1
  7. package/dist/extension/lib/conversation-runtime.mjs +1 -23
  8. package/dist/extension/lib/group-routing/addressing.d.ts +18 -0
  9. package/dist/extension/lib/group-routing/addressing.mjs +108 -0
  10. package/dist/extension/lib/group-routing/burst.mjs +1 -1
  11. package/dist/extension/lib/group-routing/conversation-prompt.d.ts +5 -2
  12. package/dist/extension/lib/group-routing/conversation-prompt.mjs +26 -6
  13. package/dist/extension/lib/group-routing/conversation-router.d.ts +8 -16
  14. package/dist/extension/lib/group-routing/conversation-router.mjs +49 -93
  15. package/dist/extension/lib/group-routing/dequeue.mjs +1 -1
  16. package/dist/extension/lib/group-routing/messages.d.ts +0 -2
  17. package/dist/extension/lib/group-routing/policy.d.ts +13 -9
  18. package/dist/extension/lib/group-routing/policy.mjs +35 -25
  19. package/dist/extension/lib/group-routing/router.d.ts +0 -2
  20. package/dist/extension/lib/group-routing/router.mjs +9 -31
  21. package/dist/extension/lib/group-routing/routing-context.mjs +0 -10
  22. package/dist/extension/lib/telegram/adapter.mjs +1 -1
  23. package/dist/extension/lib/telegram/health.mjs +1 -1
  24. package/dist/extension/lib/telegram/inbound.mjs +1 -10
  25. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  26. package/package.json +1 -1
  27. package/dist/extension/lib/group-routing/conversation.d.ts +0 -46
  28. package/dist/extension/lib/group-routing/conversation.mjs +0 -269
@@ -29,10 +29,6 @@ export interface SmartPolicy {
29
29
  readonly engagement: {
30
30
  readonly policy: "legacy" | "engaged" | "conversation";
31
31
  readonly agentAliases: ReadonlyArray<string>;
32
- readonly idleMinutes: number;
33
- readonly humanExchangeMinutes: number;
34
- readonly humanQuestionMinutes: number;
35
- readonly inferredMaxMinutes: number;
36
32
  readonly burst: {
37
33
  readonly quietSeconds: number;
38
34
  readonly maxSeconds: number;
@@ -44,11 +40,6 @@ export interface SmartPolicy {
44
40
  readonly addresseeGate: boolean;
45
41
  };
46
42
  readonly thresholds: {
47
- readonly continuation: number;
48
- readonly inferredContinuation: number;
49
- readonly contested: number;
50
- readonly ambient: number;
51
- readonly humanExchange: number;
52
43
  readonly engaged: number;
53
44
  readonly neutral: number;
54
45
  readonly humanThread: number;
@@ -67,6 +58,19 @@ export interface GroupPolicy {
67
58
  readonly sources: Readonly<Record<string, string>>;
68
59
  }
69
60
  export declare function smartPolicy(input?: SmartPolicyInput, base?: SmartPolicy): SmartPolicy;
61
+ /**
62
+ * Settings of the former conversation state machine (timers and per-state thresholds). They are
63
+ * still accepted so that existing configurations deploy, and are ignored: the classifier reads the
64
+ * chat's recent messages instead.
65
+ */
66
+ export declare const IGNORED_GROUP_SETTINGS: {
67
+ readonly engagement: readonly ["idleMinutes", "humanExchangeMinutes", "humanQuestionMinutes", "inferredMaxMinutes"];
68
+ readonly thresholds: readonly ["continuation", "inferredContinuation", "contested", "ambient", "humanExchange"];
69
+ };
70
+ /** Whether a settings path names an ignored setting, in the agent, a chat or the shadow policy. */
71
+ export declare function ignoredGroupSetting(path: ReadonlyArray<string>): boolean;
72
+ /** The dotted paths of ignored settings an authored `groups` value sets, for a deploy warning. */
73
+ export declare function ignoredGroupSettings(value: unknown, prefix?: ReadonlyArray<string>): string[];
70
74
  export declare function groupPolicy(input?: GroupPolicyInput, aliases?: {
71
75
  readonly mode?: string;
72
76
  readonly model?: string;
@@ -48,10 +48,6 @@ function smartPolicy(input = {}, base) {
48
48
  engagement: {
49
49
  policy: selected,
50
50
  agentAliases: [],
51
- idleMinutes: 10,
52
- humanExchangeMinutes: 5,
53
- humanQuestionMinutes: 30,
54
- inferredMaxMinutes: 30,
55
51
  followUpMinutes: 30,
56
52
  conversationMinutes: 10,
57
53
  ...base?.engagement,
@@ -63,7 +59,7 @@ function smartPolicy(input = {}, base) {
63
59
  openQuestionMinutes: engaged ? 30 : "unlimited",
64
60
  addresseeGate: engaged
65
61
  } : {},
66
- ...input.engagement,
62
+ ...withoutIgnored(input.engagement, IGNORED_GROUP_SETTINGS.engagement),
67
63
  burst: {
68
64
  quietSeconds: 6,
69
65
  maxSeconds: 15,
@@ -76,25 +72,44 @@ function smartPolicy(input = {}, base) {
76
72
  engaged: .5,
77
73
  neutral: conversation ? .9 : engaged ? .75 : .5,
78
74
  humanThread: conversation ? .95 : engaged ? .9 : .5,
79
- continuation: .5,
80
- inferredContinuation: .7,
81
- contested: .8,
82
- ambient: .9,
83
- humanExchange: .95,
84
75
  ...changed ? {} : base?.thresholds,
85
- ...input.thresholds,
86
- ...conversation ? {
87
- continuation: input.thresholds?.continuation ?? input.thresholds?.engaged ?? (changed ? .5 : base?.thresholds.continuation) ?? .5,
88
- ambient: input.thresholds?.ambient ?? input.thresholds?.neutral ?? (changed ? .9 : base?.thresholds.ambient) ?? .9,
89
- humanExchange: input.thresholds?.humanExchange ?? input.thresholds?.humanThread ?? (changed ? .95 : base?.thresholds.humanExchange) ?? .95
90
- } : {}
76
+ ...withoutIgnored(input.thresholds, IGNORED_GROUP_SETTINGS.thresholds)
91
77
  },
92
78
  timeoutSeconds: input.timeoutSeconds ?? base?.timeoutSeconds ?? 15
93
79
  };
94
80
  if (policy.engagement.burst.quietSeconds > policy.engagement.burst.maxSeconds) throw new RangeError("Burst quietSeconds must not exceed maxSeconds after inheritance.");
95
- if ((conversation ? policy.instructions.length + JSON.stringify(ADDRESSEE_CRITERIA).length + 260 + 185 : policy.instructions.length + policy.criteria.observe.length + policy.criteria.reply.length) > 8e3) throw new RangeError("Group instructions and criteria must fit 8000 characters after inheritance.");
81
+ if ((conversation ? policy.instructions.length + JSON.stringify(ADDRESSEE_CRITERIA).length + 260 : policy.instructions.length + policy.criteria.observe.length + policy.criteria.reply.length) > 8e3) throw new RangeError("Group instructions and criteria must fit 8000 characters after inheritance.");
96
82
  return policy;
97
83
  }
84
+ const IGNORED_GROUP_SETTINGS = {
85
+ engagement: [
86
+ "idleMinutes",
87
+ "humanExchangeMinutes",
88
+ "humanQuestionMinutes",
89
+ "inferredMaxMinutes"
90
+ ],
91
+ thresholds: [
92
+ "continuation",
93
+ "inferredContinuation",
94
+ "contested",
95
+ "ambient",
96
+ "humanExchange"
97
+ ]
98
+ };
99
+ function withoutIgnored(value, ignored) {
100
+ return Object.fromEntries(Object.entries(value ?? {}).filter(([key]) => !ignored.includes(key)));
101
+ }
102
+ const IGNORED_SUFFIXES = Object.entries(IGNORED_GROUP_SETTINGS).flatMap(([section, keys]) => keys.map((key) => `${section}.${key}`));
103
+ function ignoredGroupSetting(path) {
104
+ return IGNORED_SUFFIXES.includes(path.slice(-2).join("."));
105
+ }
106
+ function ignoredGroupSettings(value, prefix = []) {
107
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return ignoredGroupSetting(prefix) ? [prefix.join(".")] : [];
108
+ return Object.entries(value).flatMap(([key, child]) => ignoredGroupSettings(child, [...prefix, key]));
109
+ }
110
+ function authored(value, source) {
111
+ return Object.fromEntries(Object.entries(sources(value, source)).filter(([path]) => !ignoredGroupSetting(path.split("."))));
112
+ }
98
113
  function sources(value, source, prefix = "") {
99
114
  return Object.fromEntries(Object.entries(value).flatMap(([key, value]) => {
100
115
  const path = prefix ? `${prefix}.${key}` : key;
@@ -122,7 +137,7 @@ function groupPolicy(input = {}, aliases) {
122
137
  }, "platform"),
123
138
  ...aliases?.mode ? { mode: "channels.telegram.policy" } : {},
124
139
  ...aliases?.model ? { "smart.model": "models.decision" } : {},
125
- ...sources(input, "agent")
140
+ ...authored(input, "agent")
126
141
  }
127
142
  };
128
143
  }
@@ -144,11 +159,6 @@ function overlaySmartPolicy(input, base) {
144
159
  addresseeGate: inherited("engagement.addresseeGate") ? defaults.engagement.addresseeGate : base.smart.engagement.addresseeGate
145
160
  },
146
161
  thresholds: {
147
- continuation: inherited("thresholds.continuation") ? defaults.thresholds.continuation : base.smart.thresholds.continuation,
148
- inferredContinuation: inherited("thresholds.inferredContinuation") ? defaults.thresholds.inferredContinuation : base.smart.thresholds.inferredContinuation,
149
- contested: inherited("thresholds.contested") ? defaults.thresholds.contested : base.smart.thresholds.contested,
150
- ambient: inherited("thresholds.ambient") ? defaults.thresholds.ambient : base.smart.thresholds.ambient,
151
- humanExchange: inherited("thresholds.humanExchange") ? defaults.thresholds.humanExchange : base.smart.thresholds.humanExchange,
152
162
  engaged: inherited("thresholds.engaged") ? defaults.thresholds.engaged : base.smart.thresholds.engaged,
153
163
  neutral: inherited("thresholds.neutral") ? defaults.thresholds.neutral : base.smart.thresholds.neutral,
154
164
  humanThread: inherited("thresholds.humanThread") ? defaults.thresholds.humanThread : base.smart.thresholds.humanThread
@@ -169,11 +179,11 @@ function effectiveGroupPolicy(base, key) {
169
179
  },
170
180
  sources: {
171
181
  ...result.sources,
172
- ...sources(layer, "agent.chat")
182
+ ...authored(layer, "agent.chat")
173
183
  }
174
184
  };
175
185
  const thresholds = result.smart.thresholds;
176
186
  if (thresholds.engaged > thresholds.neutral || thresholds.neutral > thresholds.humanThread) throw new RangeError("Group thresholds must satisfy engaged <= neutral <= humanThread after inheritance.");
177
187
  return result;
178
188
  }
179
- export { DEFAULT_CRITERIA, DEFAULT_GROUP_POLICY, ENGAGED_CRITERIA, ENGAGED_INSTRUCTIONS, LEGACY_INSTRUCTIONS, effectiveGroupPolicy, groupPolicy, overlaySmartPolicy, smartPolicy };
189
+ export { DEFAULT_CRITERIA, DEFAULT_GROUP_POLICY, ENGAGED_CRITERIA, ENGAGED_INSTRUCTIONS, IGNORED_GROUP_SETTINGS, LEGACY_INSTRUCTIONS, effectiveGroupPolicy, groupPolicy, ignoredGroupSetting, ignoredGroupSettings, overlaySmartPolicy, smartPolicy };
@@ -1,4 +1,3 @@
1
- import { type ConversationAnalysis } from "./conversation.js";
2
1
  import { type AddressingAnswers } from "./conversation-router.js";
3
2
  import { type GroupPolicy } from "./policy.js";
4
3
  import type { ObservedMessage, PendingConfirmation, RecentMessage } from "./messages.js";
@@ -9,7 +8,6 @@ export type Route = "observe" | "reply";
9
8
  /** What the decision model sees. */
10
9
  export interface RouteQuestion {
11
10
  readonly groups?: GroupPolicy;
12
- readonly conversation?: ConversationAnalysis;
13
11
  readonly burstId?: string;
14
12
  readonly processingAt?: string;
15
13
  readonly messagesToClassify?: ReadonlyArray<ObservedMessage>;
@@ -6,8 +6,8 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { DEFAULT_GROUP_POLICY, groupPolicy, overlaySmartPolicy } from "./policy.mjs";
8
8
  import { addressingReason, otherRecipient, signalAudience } from "./signals.mjs";
9
- import { conversationState, conversationThreshold } from "./conversation.mjs";
10
- import { conversationDecisionRouter } from "./conversation-router.mjs";
9
+ import { conversationAddressing } from "./addressing.mjs";
10
+ import { addressedToAgent, conversationDecisionRouter } from "./conversation-router.mjs";
11
11
  import { recordRoutingMetric } from "./health.mjs";
12
12
  import { humanAddressee, humanThread, questionInvitation, restoreEngagement } from "./engagement.mjs";
13
13
  import { boundedRouterPayload, policyHash } from "./record.mjs";
@@ -300,25 +300,11 @@ async function routeMessage(input) {
300
300
  groups,
301
301
  signals
302
302
  };
303
- const analysis = conversationState({
304
- signals,
305
- recent: question.recent,
306
- policy: groups,
307
- agentNames: [...question.agentIdentity?.aliases ?? [], ...question.agentIdentity?.username ? [question.agentIdentity.username] : []],
308
- pendingQuestionAt: input.pendingQuestionAt,
309
- asOf: question.processingAt
310
- });
311
- question = {
312
- ...question,
313
- conversation: analysis
314
- };
303
+ const evidence = conversationAddressing(signals, question.recent, groups, [...question.agentIdentity?.aliases ?? [], ...question.agentIdentity?.username ? [question.agentIdentity.username] : []]);
315
304
  payload = {
316
305
  ...payload,
317
306
  burst_id: question.burstId,
318
- path: analysis.path,
319
- conversation: analysis.conversation,
320
- human_exchange: analysis.human_exchange,
321
- pending_question_voided: input.pendingQuestionAt !== void 0 && !analysis.pending_question_live,
307
+ addressing_evidence: evidence,
322
308
  question: {
323
309
  recent: question.recent,
324
310
  pendingQuestions: question.pendingQuestions,
@@ -328,30 +314,22 @@ async function routeMessage(input) {
328
314
  messagesToClassify: question.messagesToClassify
329
315
  }
330
316
  };
331
- if (analysis.path === "A") {
317
+ if (evidence.agent_named || evidence.reply_to_agent && evidence.human_targets.length === 0 && evidence.soft_target === void 0) {
332
318
  if (input.beforeDecide && !await input.beforeDecide(true)) return complete("observe", "admission_refused", "Admission refused before direct turn.");
333
319
  return complete("reply", addressed && addressed !== "pending-question" ? addressed : "mention", "Agent explicitly addressed.");
334
320
  }
335
- if (analysis.path === "H") return complete("observe", "human_addressee", "Message addresses another participant; conversation and question invitation closed.");
321
+ if (evidence.human_targets.length > 0) return complete("observe", "human_addressee", "Message replies to, tags or names another participant.");
336
322
  if (signals.addressing.eligible === false) return complete("observe", "channel_ineligible", "Channel excludes ambient messages.");
337
323
  if (groups.mode === "mention") return complete("observe", "mention_policy", "Mention policy observes unaddressed messages.");
338
324
  if (input.beforeDecide && !await input.beforeDecide()) return complete("observe", "admission_refused", "Admission refused before inference.");
339
325
  const result = await input.decide(question);
340
326
  const diagnostics = typeof result === "string" ? void 0 : result.diagnostics;
341
- const probabilities = diagnostics?.addressing;
342
- const threshold = conversationThreshold(analysis.path, groups);
343
- const valid = probabilities !== void 0 && probabilities.addressee.choice === "agent" && (probabilities.addressee.probabilities?.agent ?? NaN) >= threshold && (probabilities.needs_response.probabilities?.true ?? NaN) >= (analysis.path === "B" || analysis.path === "X" ? .7 : .5) && (probabilities.ends_conversation.probabilities?.true ?? NaN) < .7;
327
+ const valid = addressedToAgent(diagnostics?.addressing);
344
328
  payload = {
345
329
  ...payload,
346
- ...typeof result === "string" ? {} : result.router,
347
- thresholds_applied: {
348
- threshold,
349
- path: analysis.path,
350
- needs_response: analysis.path === "B" || analysis.path === "X" ? .7 : .5,
351
- ends_conversation: .7
352
- }
330
+ ...typeof result === "string" ? {} : result.router
353
331
  };
354
- return complete(valid ? "reply" : "observe", `decision_model_${valid ? "reply" : "observe"}`, "Conversation addressing probabilities evaluated.", "decision-model", diagnostics);
332
+ return complete(valid ? "reply" : "observe", `decision_model_${valid ? "reply" : "observe"}`, "Addressee read from the chat's recent messages.", "decision-model", diagnostics);
355
333
  } catch (error) {
356
334
  console.error("[segmentstream] conversation routing failed", { error });
357
335
  return complete("observe", error instanceof Error && error.name === "TimeoutError" ? "decision_model_timeout" : "decision_model_failed", "Conversation routing unavailable; observe fallback.");
@@ -34,15 +34,6 @@ function archivedRoutingMessages(events, limit = 30) {
34
34
  "thread-with-agent",
35
35
  "pending-question"
36
36
  ].includes(event.reason ?? "")).map((event) => [event.messageId, event.reason]));
37
- const ended = new Set(events.filter((event) => {
38
- if (event.kind !== "decision" || event.messageId === null) return false;
39
- try {
40
- const data = JSON.parse(event.argsSummary ?? "{}");
41
- return data.router?.conversation_ended === true || data.routing?.conversationEnded === true;
42
- } catch {
43
- return false;
44
- }
45
- }).map((event) => event.messageId));
46
37
  const admitted = new Set(events.flatMap((event) => event.kind === "decision" && event.reason === "turn_admitted" && event.messageId !== null ? [event.messageId] : []));
47
38
  const membership = new Map();
48
39
  for (const event of [...events].reverse()) {
@@ -80,7 +71,6 @@ function archivedRoutingMessages(events, limit = 30) {
80
71
  invitation: metadata.invitation ?? invitations.get(event.messageId ?? ""),
81
72
  ...event.kind === "inbound" && admitted.has(event.messageId ?? "") ? { admitted: true } : {},
82
73
  ...images.length === 0 ? {} : { images },
83
- ...ended.has(event.messageId) ? { conversationEnded: true } : {},
84
74
  author: event.senderName ?? event.senderId ?? "unknown sender",
85
75
  ...typeof metadata.author === "string" && event.kind === "inbound" ? { signedAuthor: metadata.author } : {},
86
76
  text,
@@ -23,7 +23,7 @@ import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
23
23
  import { dispatchStructuredMessage, structuredMessage } from "./structured.mjs";
24
24
  import { scopedAddress } from "./sessions.mjs";
25
25
  import { handleWorkspaceCallback, isWorkspaceCallback } from "./callbacks.mjs";
26
- import { t as agentBuild } from "../../../_chunks/build-metadata-R_ssK2uR.mjs";
26
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
27
27
  import { attachmentLifecycle } from "./lifecycle.mjs";
28
28
  import { gatewayImageDescriber } from "./images.mjs";
29
29
  import { gatewayAttachmentDescriber } from "./attachment-description.mjs";
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { runtimeRequestFailure } from "./runtime-request.mjs";
8
- import { t as agentBuild } from "../../../_chunks/build-metadata-R_ssK2uR.mjs";
8
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
11
11
  function healthRoute(input) {
@@ -9,7 +9,7 @@ import { admitMessage, localizedText } from "../admission.mjs";
9
9
  import { d as telegramRuntimeSource, n as conversationLanguage, o as telegramPrincipal, u as runtimeEnvelope } from "../../../_chunks/principal-D99Lleae.mjs";
10
10
  import { p as reportTelegramError } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
11
11
  import { addressingReason, conversationKey, otherRecipient } from "../group-routing/signals.mjs";
12
- import { conversationAddressing, conversationState } from "../group-routing/conversation.mjs";
12
+ import { conversationAddressing } from "../group-routing/addressing.mjs";
13
13
  import { questionInvitation } from "../group-routing/engagement.mjs";
14
14
  import { routeMessage } from "../group-routing/router.mjs";
15
15
  import { debounceBurst, routingBurst } from "../group-routing/burst.mjs";
@@ -515,15 +515,6 @@ async function handleInboundMessage(input) {
515
515
  });
516
516
  const questionAge = Date.parse(sentAt(sent)) - Date.parse(asked?.meta.at ?? "");
517
517
  if (answering && addressed === void 0 && !questionInvitation(asked?.meta.at, sentAt(sent), groups.smart.engagement.openQuestionMinutes)) answering = false;
518
- if (conversationPolicy && answering) {
519
- if (!conversationState({
520
- signals: policySignals,
521
- recent: conversationRecent,
522
- policy: groups,
523
- agentNames: agentAliases,
524
- pendingQuestionAt: asked?.meta.at
525
- }).pending_question_live) answering = false;
526
- }
527
518
  const recipientAnswer = !conversationPolicy && answering && (groups.smart.engagement.policy === "engaged" || questionAge >= 0 && questionAge <= groups.smart.engagement.followUpMinutes * 6e4) && audience.replyTo?.kind === "bot" && isRecipientAnswer(audience);
528
519
  if (!conversationPolicy && answering && pending !== void 0 && input.request !== void 0) {
529
520
  address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
@@ -10,7 +10,7 @@ import { n as currentInboundRequest } from "../../../_chunks/inbound-request-D1r
10
10
  import { member } from "../actor.mjs";
11
11
  import { needsConfirmation } from "../confirmations.mjs";
12
12
  import { OntologyActionNotPermitted, actionToolName, ontologyTools } from "../ontology-tools.mjs";
13
- import { t as agentBuild } from "../../../_chunks/build-metadata-R_ssK2uR.mjs";
13
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CMmCRD8l.mjs";
14
14
  var AttachmentLifecycleNotPermitted = class extends Error {
15
15
  code = "attachment_lifecycle_not_permitted";
16
16
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.27.0",
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 "./messages.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 };