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