@segmentstream/agent 0.26.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +41 -43
  2. package/dist/_chunks/{build-metadata-D_Gqlb5f.mjs → build-metadata-CMmCRD8l.mjs} +1 -1
  3. package/dist/extension/config.d.ts +11 -12
  4. package/dist/extension/config.mjs +20 -37
  5. package/dist/extension/extension.d.ts +2 -2
  6. package/dist/extension/extension.mjs +3 -3
  7. package/dist/extension/hooks/chat-context.d.ts +3 -0
  8. package/dist/extension/hooks/chat-context.mjs +10 -0
  9. package/dist/extension/hooks/sandbox.d.ts +2 -3
  10. package/dist/extension/instructions/conversations.d.ts +3 -2
  11. package/dist/extension/instructions/conversations.mjs +7 -11
  12. package/dist/extension/instructions.md +18 -0
  13. package/dist/extension/lib/build-metadata.mjs +1 -1
  14. package/dist/extension/lib/chat-context.d.ts +108 -0
  15. package/dist/extension/lib/chat-context.mjs +217 -0
  16. package/dist/extension/lib/conversation-runtime.d.ts +1 -1
  17. package/dist/extension/lib/conversation-runtime.mjs +1 -23
  18. package/dist/extension/lib/conversations.d.ts +10 -6
  19. package/dist/extension/lib/conversations.mjs +28 -42
  20. package/dist/extension/lib/group-routing/addressing.d.ts +18 -0
  21. package/dist/extension/lib/group-routing/addressing.mjs +108 -0
  22. package/dist/extension/lib/group-routing/burst.d.ts +1 -1
  23. package/dist/extension/lib/group-routing/burst.mjs +1 -1
  24. package/dist/extension/lib/group-routing/conversation-prompt.d.ts +5 -2
  25. package/dist/extension/lib/group-routing/conversation-prompt.mjs +26 -6
  26. package/dist/extension/lib/group-routing/conversation-router.d.ts +8 -16
  27. package/dist/extension/lib/group-routing/conversation-router.mjs +49 -93
  28. package/dist/extension/lib/group-routing/dequeue.mjs +1 -1
  29. package/dist/extension/lib/group-routing/engagement.d.ts +1 -1
  30. package/dist/extension/lib/group-routing/{observed.d.ts → messages.d.ts} +9 -33
  31. package/dist/extension/lib/group-routing/messages.mjs +38 -0
  32. package/dist/extension/lib/group-routing/policy.d.ts +13 -9
  33. package/dist/extension/lib/group-routing/policy.mjs +35 -25
  34. package/dist/extension/lib/group-routing/router.d.ts +3 -11
  35. package/dist/extension/lib/group-routing/router.mjs +11 -35
  36. package/dist/extension/lib/group-routing/routing-context.d.ts +6 -2
  37. package/dist/extension/lib/group-routing/routing-context.mjs +12 -11
  38. package/dist/extension/lib/telegram/adapter.d.ts +1 -3
  39. package/dist/extension/lib/telegram/adapter.mjs +9 -18
  40. package/dist/extension/lib/telegram/confirmations.d.ts +3 -3
  41. package/dist/extension/lib/telegram/confirmations.mjs +3 -25
  42. package/dist/extension/lib/telegram/conversations.d.ts +14 -3
  43. package/dist/extension/lib/telegram/conversations.mjs +16 -37
  44. package/dist/extension/lib/telegram/delivery-diagnostics.d.ts +4 -1
  45. package/dist/extension/lib/telegram/delivery-diagnostics.mjs +9 -3
  46. package/dist/extension/lib/telegram/health.mjs +1 -1
  47. package/dist/extension/lib/telegram/inbound.d.ts +14 -14
  48. package/dist/extension/lib/telegram/inbound.mjs +73 -78
  49. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  50. package/dist/extension/lib/telegram/{observed.d.ts → message-metadata.d.ts} +2 -6
  51. package/dist/extension/lib/telegram/{observed.mjs → message-metadata.mjs} +1 -14
  52. package/dist/extension/lib/telegram/routing-context.d.ts +4 -3
  53. package/dist/extension/lib/telegram/routing-context.mjs +2 -2
  54. package/dist/extension/lib/telegram/sessions.d.ts +5 -52
  55. package/dist/extension/lib/telegram/sessions.mjs +1 -188
  56. package/dist/extension/lib/telegram/signals.mjs +1 -1
  57. package/dist/extension/lib/telegram/silent.d.ts +5 -6
  58. package/dist/extension/lib/telegram/silent.mjs +4 -31
  59. package/package.json +1 -1
  60. package/dist/extension/lib/group-routing/conversation.d.ts +0 -46
  61. package/dist/extension/lib/group-routing/conversation.mjs +0 -269
  62. package/dist/extension/lib/group-routing/observed.mjs +0 -103
@@ -1,4 +1,3 @@
1
- import { type ConversationAnalysis } from "./conversation.js";
2
1
  import type { RouteQuestion, RouteDecider } from "./router.js";
3
2
  export interface AddressingAnswers {
4
3
  readonly addressee: {
@@ -11,13 +10,13 @@ export interface AddressingAnswers {
11
10
  readonly type?: "boolean";
12
11
  readonly probabilities?: Readonly<Record<"true" | "false", number>>;
13
12
  };
14
- readonly ends_conversation: {
15
- readonly choice?: boolean;
16
- readonly probability?: number;
17
- readonly type?: "boolean";
18
- readonly probabilities?: Readonly<Record<"true" | "false", number>>;
19
- };
20
13
  }
14
+ /** The agent answers when it is the likeliest addressee, by a majority, and a reply is needed. */
15
+ export declare const ADDRESSED_TO_AGENT: {
16
+ readonly addressee: 0.5;
17
+ readonly needsResponse: 0.5;
18
+ };
19
+ export declare function addressedToAgent(answers: AddressingAnswers | undefined): boolean;
21
20
  export declare function conversationQuestions(instructions: string): {
22
21
  addressee: {
23
22
  type: "choice";
@@ -37,14 +36,6 @@ export declare function conversationQuestions(instructions: string): {
37
36
  false: string;
38
37
  };
39
38
  };
40
- ends_conversation: {
41
- type: "boolean";
42
- instructions: string;
43
- criteria: {
44
- true: string;
45
- false: string;
46
- };
47
- };
48
39
  };
49
40
  export type ConversationDecideFunction = (options: {
50
41
  readonly model: string;
@@ -54,5 +45,6 @@ export type ConversationDecideFunction = (options: {
54
45
  }) => Promise<{
55
46
  readonly answers: AddressingAnswers;
56
47
  }>;
57
- export declare function conversationModelState(question: RouteQuestion, analysis: ConversationAnalysis): Parameters<ConversationDecideFunction>[0]["state"];
48
+ /** The chat's latest messages as the classifier reads them, however old: no verdicts, only facts. */
49
+ export declare function conversationModelState(question: RouteQuestion): Parameters<ConversationDecideFunction>[0]["state"];
58
50
  export declare function conversationDecisionRouter(decide: ConversationDecideFunction): RouteDecider;
@@ -4,8 +4,15 @@ 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 { ADDRESSEE_CRITERIA, ENDS_CONVERSATION_INSTRUCTIONS, NEEDS_RESPONSE_INSTRUCTIONS } from "./conversation-prompt.mjs";
8
- import { conversationState, conversationThreshold } from "./conversation.mjs";
7
+ import { ADDRESSEE_CRITERIA, NEEDS_RESPONSE_INSTRUCTIONS } from "./conversation-prompt.mjs";
8
+ const ADDRESSED_TO_AGENT = {
9
+ addressee: .5,
10
+ needsResponse: .5
11
+ };
12
+ const RECENT_CHARACTERS = 24e3;
13
+ function addressedToAgent(answers) {
14
+ return answers !== void 0 && answers.addressee.choice === "agent" && (answers.addressee.probabilities?.agent ?? NaN) >= ADDRESSED_TO_AGENT.addressee && (answers.needs_response.probabilities?.true ?? NaN) >= ADDRESSED_TO_AGENT.needsResponse;
15
+ }
9
16
  function conversationQuestions(instructions) {
10
17
  return {
11
18
  addressee: {
@@ -20,28 +27,38 @@ function conversationQuestions(instructions) {
20
27
  true: "Asks for or needs an answer or action.",
21
28
  false: "Acknowledgement, reaction, status, closing or remark."
22
29
  }
23
- },
24
- ends_conversation: {
25
- type: "boolean",
26
- instructions: ENDS_CONVERSATION_INSTRUCTIONS,
27
- criteria: {
28
- true: "Ends or declines the agent's involvement.",
29
- false: "Anything else."
30
- }
31
30
  }
32
31
  };
33
32
  }
34
- function conversationModelState(question, analysis) {
33
+ function conversationModelState(question) {
35
34
  const groups = question.groups;
36
35
  const now = Date.parse(question.processingAt ?? question.message.at);
37
36
  const own = (m) => "role" in m && m.role === "agent";
38
- const classifiedIds = new Set((question.messagesToClassify ?? [question.message]).map((m) => m.messageId));
39
- const conversationStart = analysis.conversation.status === "active" && analysis.conversation.opened_at !== void 0 ? Date.parse(analysis.conversation.opened_at) : now - 9e5;
40
- const recent = question.recent.filter((m) => !classifiedIds.has(m.messageId)).filter((m) => now - Date.parse(m.at) >= 0 && now - Date.parse(m.at) <= 9e5 && Date.parse(m.at) >= conversationStart).slice(-Math.min(20, groups.smart.context.messages));
41
- const last = recent.findLast((m) => own(m) && "addressedTo" in m && m.addressedTo.some((p) => p.id === question.signals?.sender.externalId || p.username && p.username === question.signals?.sender.username));
42
- const pending = analysis.pending_question_live ? question.pendingQuestions?.at(-1) : void 0;
37
+ const classified = question.messagesToClassify ?? [question.message];
38
+ const classifiedIds = new Set(classified.map((m) => m.messageId));
39
+ const format = (m, media) => JSON.stringify({
40
+ at: m.at,
41
+ id: m.messageId ?? null,
42
+ author: own(m) ? "THE AGENT" : m.author,
43
+ author_id: m.authorId ?? null,
44
+ text: m.text.slice(0, 1e3),
45
+ media: m.media?.slice(0, media),
46
+ reply_to: m.replyTo ?? null,
47
+ tags: m.taggedUsers ?? [],
48
+ attachments: m.attachmentKinds ?? []
49
+ });
50
+ const candidates = question.recent.filter((m) => !classifiedIds.has(m.messageId) && Date.parse(m.at) <= now).slice(-groups.smart.context.messages);
51
+ const lines = [];
52
+ let characters = 0;
53
+ for (const m of [...candidates].reverse()) {
54
+ const line = format(m, 500);
55
+ if (lines.length > 0 && characters + line.length > RECENT_CHARACTERS) break;
56
+ lines.unshift(line);
57
+ characters += line.length;
58
+ }
59
+ const recent = candidates.slice(candidates.length - lines.length);
43
60
  const roster = new Map();
44
- for (const m of [...recent, question.message]) {
61
+ for (const m of [...recent, ...classified]) {
45
62
  if (own(m)) continue;
46
63
  const id = m.authorId ?? m.username ?? m.author;
47
64
  roster.set(id, {
@@ -52,16 +69,6 @@ function conversationModelState(question, analysis) {
52
69
  kind: m.kind ?? "human"
53
70
  });
54
71
  }
55
- const format = (m) => JSON.stringify({
56
- at: m.at,
57
- author: own(m) ? "THE AGENT" : m.author,
58
- author_id: m.authorId ?? null,
59
- text: m.text.slice(0, 1e3),
60
- media: m.media?.slice(0, 2e3),
61
- reply_to: m.replyTo ?? null,
62
- tags: m.taggedUsers ?? [],
63
- attachments: m.attachmentKinds ?? []
64
- });
65
72
  return {
66
73
  agent: JSON.stringify({
67
74
  names: [...new Set([
@@ -77,47 +84,14 @@ function conversationModelState(question, analysis) {
77
84
  purpose: (groups.smart.context.includeChatGuidance ? question.chat ?? [] : []).join("\n").slice(0, 500)
78
85
  }),
79
86
  participants: groups.smart.context.includeParticipants ? [...roster.values()].map((p) => JSON.stringify(p)) : [],
80
- route_context: JSON.stringify({
81
- path: {
82
- A: "explicit",
83
- H: "human_target",
84
- C: "continuation",
85
- Ci: "inferred_continuation",
86
- C2: "contested",
87
- B: "ambient",
88
- X: "human_exchange"
89
- }[analysis.path],
90
- conversation_with_sender: {
91
- ...analysis.conversation,
92
- seconds_since_last_exchange: analysis.conversation.last_exchange_at ? Math.floor((now - Date.parse(analysis.conversation.last_exchange_at)) / 1e3) : null,
93
- agent_last_message_to_sender: last ? {
94
- text: last.text.slice(0, 600),
95
- seconds_ago: Math.floor((now - Date.parse(last.at)) / 1e3)
96
- } : null,
97
- agent_open_question: pending ? {
98
- text: pending.question.slice(0, 300),
99
- seconds_ago: pending.at ? Math.floor((now - Date.parse(pending.at)) / 1e3) : null
100
- } : null
101
- },
102
- human_exchange: analysis.human_exchange,
103
- other_live_agent_conversations: analysis.other_live_agent_conversations
104
- }),
105
- recent_messages: recent.map(format),
106
- messages_to_classify: (question.messagesToClassify ?? [question.message]).map(format)
87
+ recent_messages: lines,
88
+ messages_to_classify: classified.map((m) => format(m, 2e3))
107
89
  };
108
90
  }
109
91
  function conversationDecisionRouter(decide) {
110
92
  return async (question) => {
111
93
  const policy = question.groups;
112
- const analysis = question.conversation ?? conversationState({
113
- signals: question.signals,
114
- recent: question.recent,
115
- policy,
116
- agentNames: question.agentIdentity?.aliases,
117
- pendingQuestionAt: question.pendingQuestions?.at(-1)?.at,
118
- asOf: question.processingAt
119
- });
120
- const state = conversationModelState(question, analysis);
94
+ const state = conversationModelState(question);
121
95
  const questions = conversationQuestions(policy.smart.instructions);
122
96
  const started = performance.now();
123
97
  const signal = AbortSignal.timeout(Math.ceil(policy.smart.timeoutSeconds * 1e3));
@@ -127,56 +101,38 @@ function conversationDecisionRouter(decide) {
127
101
  questions,
128
102
  signal
129
103
  });
130
- const normalize = (answer) => {
131
- const p = answer.probabilities?.true ?? answer.probability ?? NaN;
132
- return {
104
+ const p = raw.needs_response.probabilities?.true ?? raw.needs_response.probability ?? NaN;
105
+ const answers = {
106
+ ...raw,
107
+ needs_response: {
133
108
  choice: p >= .5,
134
109
  probabilities: {
135
110
  true: p,
136
111
  false: 1 - p
137
112
  }
138
- };
139
- };
140
- const answers = {
141
- ...raw,
142
- needs_response: normalize(raw.needs_response),
143
- ends_conversation: normalize(raw.ends_conversation)
113
+ }
144
114
  };
145
- const p = answers.addressee.probabilities?.agent ?? NaN, needs = answers.needs_response.probabilities?.true ?? NaN, ends = answers.ends_conversation.probabilities?.true ?? NaN;
146
- if ([
147
- p,
148
- needs,
149
- ends
150
- ].some((x) => !Number.isFinite(x) || x < 0 || x > 1)) throw new Error("Conversation classifier probabilities unavailable.");
151
- const threshold = conversationThreshold(analysis.path, policy);
152
- const reply = answers.addressee.choice === "agent" && p >= threshold && needs >= (analysis.path === "B" || analysis.path === "X" ? .7 : .5) && ends < .7;
115
+ const agent = answers.addressee.probabilities?.agent ?? NaN;
116
+ if ([agent, p].some((x) => !Number.isFinite(x) || x < 0 || x > 1)) throw new Error("Conversation classifier probabilities unavailable.");
153
117
  const diagnostics = {
154
118
  model: policy.smart.model,
155
119
  latencyMs: Math.round(performance.now() - started),
156
120
  probabilities: {
157
- reply: p,
158
- observe: 1 - p
121
+ reply: agent,
122
+ observe: 1 - agent
159
123
  },
160
124
  addressing: answers
161
125
  };
162
126
  return {
163
- route: reply ? "reply" : "observe",
127
+ route: addressedToAgent(answers) ? "reply" : "observe",
164
128
  diagnostics,
165
129
  router: {
166
130
  state,
167
131
  questions,
168
132
  answer: answers,
169
- path: analysis.path,
170
- conversation: analysis.conversation,
171
- human_exchange: analysis.human_exchange,
172
- thresholds_applied: {
173
- threshold,
174
- needs_response: analysis.path === "B" || analysis.path === "X" ? .7 : .5,
175
- ends_conversation: .7
176
- },
177
- conversation_ended: ends >= .7
133
+ thresholds_applied: ADDRESSED_TO_AGENT
178
134
  }
179
135
  };
180
136
  };
181
137
  }
182
- export { conversationDecisionRouter, conversationModelState, conversationQuestions };
138
+ export { ADDRESSED_TO_AGENT, addressedToAgent, conversationDecisionRouter, conversationModelState, conversationQuestions };
@@ -6,7 +6,7 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversationEvent, conversations } from "../conversations.mjs";
8
8
  import { addressingReason } from "./signals.mjs";
9
- import { recordedMessageSignals } from "./conversation.mjs";
9
+ import { recordedMessageSignals } from "./addressing.mjs";
10
10
  import { conversationDecisionRouter } from "./conversation-router.mjs";
11
11
  import { routeMessage } from "./router.mjs";
12
12
  import { archivedRoutingMessages } from "./routing-context.mjs";
@@ -1,6 +1,6 @@
1
1
  import type { GroupPolicy, Participants } from "./policy.js";
2
2
  import type { GroupMessageSignals } from "./signals.js";
3
- import type { RecentMessage } from "./observed.js";
3
+ import type { RecentMessage } from "./messages.js";
4
4
  export interface Engagement {
5
5
  readonly memberId: string;
6
6
  readonly since: string;
@@ -1,27 +1,23 @@
1
1
  /**
2
- * Bounded group conversation context for routing, and the backlog observed without answering.
3
- * A reply drains only the backlog: recent member messages and the agent's own replies remain
4
- * available to decide whether a later unaddressed message answers one of the agent's questions.
5
- *
6
- * eve's `defineState` would be the durable home, but it is session state and needs an active eve
7
- * context; an observed message has neither, because observing must not start a session. The ring
8
- * lives in process memory, per chat and forum topic; webhook capture restores it from the existing
9
- * conversation archive after a cold start.
2
+ * Group conversation messages as routing reads them from the conversation archive: received
3
+ * messages, observed or answered, and the agent's delivered replies and confirmation cards.
10
4
  */
11
5
  import type { Attachment } from "../attachments.js";
12
6
  import type { MessageAudience } from "./signals.js";
13
- /** How many observed messages a chat keeps; the oldest goes first. */
14
- export declare const OBSERVED_MESSAGES = 30;
7
+ /** How many recent messages routing reads by default. */
8
+ export declare const RECENT_MESSAGES = 30;
15
9
  export interface ObservedMessage {
16
10
  /** How the chat names the author: their username, else their first name. */
17
11
  readonly author: string;
12
+ /** How intake named the sender: a linked member's name and roles, then their handle. */
13
+ readonly signedAuthor?: string;
18
14
  readonly invitation?: string;
19
- /** Classifier rejection closes routing hints without cancelling the underlying task. */
20
- readonly conversationEnded?: boolean;
21
15
  readonly text: string;
22
16
  readonly media?: string;
23
- /** References only; never cached image bytes or a sender's private download grant. */
17
+ /** Stored native images, by reference only: never image bytes or a sender's download grant. */
24
18
  readonly images?: ReadonlyArray<Pick<Attachment, "id" | "filename" | "contentType" | "size" | "expiresAt">>;
19
+ /** The message started a turn, so a session received it. */
20
+ readonly admitted?: true;
25
21
  /** ISO time the message was sent. */
26
22
  readonly at: string;
27
23
  /** Stable Telegram identity, independent of changes to the member's display name or roles. */
@@ -72,18 +68,6 @@ export interface ConfirmationPrompt extends PendingConfirmation {
72
68
  export declare function withConfirmationPrompt<T>(prompt: ConfirmationPrompt, run: () => T): T;
73
69
  export declare function currentConfirmationPrompt(): ConfirmationPrompt | undefined;
74
70
  export type RecentMessage = ObservedMessage | AgentMessage;
75
- export interface ObservedMessages {
76
- add(chat: string, message: ObservedMessage): void;
77
- /** Records a received message that starts a turn, or a successfully delivered agent reply. */
78
- remember(chat: string, message: RecentMessage): void;
79
- list(chat: string): ReadonlyArray<ObservedMessage>;
80
- recent(chat: string): ReadonlyArray<RecentMessage>;
81
- /** Returns and forgets the chat's ring, once a reply has carried it into a turn. */
82
- take(chat: string): ReadonlyArray<ObservedMessage>;
83
- /** An idle reset forgets both the backlog and the earlier conversation. */
84
- clear(chat: string): void;
85
- }
86
- export declare function observedMessages(capacity?: number): ObservedMessages;
87
71
  /** Recipient metadata for an agent reply; explicit mentions are visible even in proactive turns. */
88
72
  export declare function agentMessage(input: {
89
73
  readonly text: string;
@@ -92,11 +76,3 @@ export declare function agentMessage(input: {
92
76
  readonly messageId?: string;
93
77
  readonly replyTo?: ObservedMessage["replyTo"];
94
78
  }): AgentMessage;
95
- /** The key of a chat's ring: the chat and, in a forum, the topic. */
96
- export declare function observedChatKey(input: {
97
- readonly chatId: string;
98
- readonly messageThreadId?: number;
99
- readonly provider?: string;
100
- }): string;
101
- /** The ring as turn context: one block the model reads before the message it answers. */
102
- export declare function observedContext(messages: ReadonlyArray<ObservedMessage>, conversation?: Readonly<Record<string, string | ReadonlyArray<string>>>): ReadonlyArray<string>;
@@ -0,0 +1,38 @@
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 { AsyncLocalStorage } from "node:async_hooks";
8
+ const RECENT_MESSAGES = 30;
9
+ const MAXIMUM_TEXT_LENGTH = 1e3;
10
+ const confirmationPrompt = new AsyncLocalStorage();
11
+ function withConfirmationPrompt(prompt, run) {
12
+ return confirmationPrompt.run(prompt, run);
13
+ }
14
+ function currentConfirmationPrompt() {
15
+ return confirmationPrompt.getStore();
16
+ }
17
+ function agentMessage(input) {
18
+ const usernames = Array.from(input.text.matchAll(/(?:^|[^A-Za-z0-9_])@([A-Za-z0-9_]+)/gu), (match) => match[1] ?? "");
19
+ const addressedTo = usernames.length > 0 ? [...new Set(usernames)].map((username) => ({
20
+ author: `@${username}`,
21
+ username
22
+ })) : input.inReplyTo === void 0 ? [] : [input.inReplyTo];
23
+ return {
24
+ ...input,
25
+ text: input.text.slice(0, MAXIMUM_TEXT_LENGTH),
26
+ author: "the agent",
27
+ role: "agent",
28
+ kind: "bot",
29
+ taggedUsers: [...new Set(usernames)].slice(0, 50).map((username) => ({
30
+ type: "mention",
31
+ kind: "unknown",
32
+ username,
33
+ text: `@${username}`
34
+ })),
35
+ addressedTo
36
+ };
37
+ }
38
+ export { RECENT_MESSAGES, agentMessage, currentConfirmationPrompt, withConfirmationPrompt };
@@ -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,7 +1,6 @@
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
- import type { ObservedMessage, PendingConfirmation, RecentMessage } from "./observed.js";
3
+ import type { ObservedMessage, PendingConfirmation, RecentMessage } from "./messages.js";
5
4
  import { type MessageAudience, type AgentIdentity as BotIdentity, type AddressingReason, type GroupMessageSignals } from "./signals.js";
6
5
  import type { PendingQuestion } from "../questions.js";
7
6
  export type { AddressingReason } from "./signals.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>;
@@ -84,14 +82,8 @@ export type DecideFunction = (options: {
84
82
  /** One decision should take a moment; a slow one gives up and observes. */
85
83
  /** An unthreaded answer minutes later may continue a question, but old questions are not hints. */
86
84
  export declare const FOLLOW_UP_WINDOW_MS: number;
87
- /**
88
- * Shared bounded conversation evidence for routing and the main model. Facts are hints only.
89
- * With `since`, `recent_messages` keep only messages at or after that time, for a main-model turn
90
- * whose session history already holds what earlier turns received; routing omits it.
91
- */
92
- export declare function conversationContext(question: RouteQuestion, options?: {
93
- readonly since?: number;
94
- }): Parameters<DecideFunction>[0]["state"];
85
+ /** Bounded conversation evidence for the routing decision. Facts are hints only. */
86
+ export declare function conversationContext(question: RouteQuestion): Parameters<DecideFunction>[0]["state"];
95
87
  /** A {@link RouteDecider} that asks one `decide` question on the configured decision model. */
96
88
  export declare function decisionRouter(input: {
97
89
  readonly decide: DecideFunction;