@segmentstream/agent 0.27.0 → 0.29.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/dist/_chunks/{build-metadata-R_ssK2uR.mjs → build-metadata-DZs-ivRD.mjs} +1 -1
- package/dist/extension/config.mjs +7 -1
- package/dist/extension/extension.d.ts +2 -2
- package/dist/extension/extension.mjs +3 -3
- package/dist/extension/lib/build-metadata.mjs +1 -1
- package/dist/extension/lib/conversation-runtime.d.ts +1 -1
- package/dist/extension/lib/conversation-runtime.mjs +1 -23
- 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 +0 -17
- package/dist/extension/lib/group-routing/burst.mjs +2 -18
- 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/messages.d.ts +0 -2
- package/dist/extension/lib/group-routing/policy.d.ts +15 -11
- package/dist/extension/lib/group-routing/policy.mjs +37 -29
- package/dist/extension/lib/group-routing/router.d.ts +0 -2
- package/dist/extension/lib/group-routing/router.mjs +9 -31
- package/dist/extension/lib/group-routing/routing-context.mjs +0 -10
- package/dist/extension/lib/telegram/adapter.mjs +1 -1
- package/dist/extension/lib/telegram/health.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
- package/dist/extension/lib/telegram/inbound.mjs +1 -62
- package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
- package/dist/extension/lib/telegram/turn-errors.mjs +0 -19
- package/package.json +1 -1
- package/dist/extension/hooks/group-routing.d.ts +0 -2
- package/dist/extension/hooks/group-routing.mjs +0 -10
- package/dist/extension/lib/group-routing/cancellation.d.ts +0 -4
- package/dist/extension/lib/group-routing/cancellation.mjs +0 -22
- 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/dequeue.d.ts +0 -5
- package/dist/extension/lib/group-routing/dequeue.mjs +0 -145
|
@@ -9,7 +9,6 @@ import { i as memberIdOf } from "../../../_chunks/principal-D99Lleae.mjs";
|
|
|
9
9
|
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
10
10
|
import { c as customerCopy, d as failureText, o as currentLocale, p as reportTelegramError, s as currentThreadId } from "../../../_chunks/inbound-request-D1r0FkDI.mjs";
|
|
11
11
|
import { recordSessionFailure } from "./conversations.mjs";
|
|
12
|
-
import { isSilentRoutingCancellation } from "../group-routing/cancellation.mjs";
|
|
13
12
|
import { modelServiceFailure } from "../model-service-failure.mjs";
|
|
14
13
|
function telegramFailureEvents(input) {
|
|
15
14
|
return {
|
|
@@ -49,15 +48,6 @@ function telegramFailureEvents(input) {
|
|
|
49
48
|
const authorizationTurn = state.authorizationCancellationTurnId;
|
|
50
49
|
delete state.authorizationCancellationTurnId;
|
|
51
50
|
if (typeof event.turnId === "string" && authorizationTurn === event.turnId) return;
|
|
52
|
-
try {
|
|
53
|
-
if (await isSilentRoutingCancellation(input.archive, context, event.turnId)) return;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
reportTelegramError("routing_cancellation_lookup_failed", error, {
|
|
56
|
-
sessionId: context.session.id,
|
|
57
|
-
turnId: event.turnId
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
51
|
const errorId = reportTelegramError("turn_cancelled", event, {
|
|
62
52
|
chatId: channel.state.chatId,
|
|
63
53
|
sessionId: context.session.id,
|
|
@@ -73,15 +63,6 @@ function telegramFailureEvents(input) {
|
|
|
73
63
|
sessionId: context.session.id,
|
|
74
64
|
turnId: event.turnId
|
|
75
65
|
});
|
|
76
|
-
try {
|
|
77
|
-
if (await isSilentRoutingCancellation(input.archive, context, event.turnId)) return;
|
|
78
|
-
} catch (error) {
|
|
79
|
-
reportTelegramError("routing_failure_lookup_failed", error, {
|
|
80
|
-
sessionId: context.session.id,
|
|
81
|
-
turnId: event.turnId
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
66
|
await channel.telegram.post(failureText(errorId, modelServiceFailure(event.error) ?? "failed", confirmationLocale(context.session.auth.current, currentLocale())));
|
|
86
67
|
},
|
|
87
68
|
async "session.ended"(fact, context) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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,10 +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
|
-
import { conversationDequeueGuard } from "../lib/group-routing/dequeue.mjs";
|
|
8
|
-
import { defineHook } from "eve/hooks";
|
|
9
|
-
var group_routing_default = defineHook({ events: { "turn.started": conversationDequeueGuard() } });
|
|
10
|
-
export { group_routing_default as default };
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { SessionContext } from "eve/context";
|
|
2
|
-
import type { Conversations } from "../conversations.js";
|
|
3
|
-
/** The durable routing verdict owns silence, including after a channel callback is replayed. */
|
|
4
|
-
export declare function isSilentRoutingCancellation(archive: Conversations, context: SessionContext, turnId: string): Promise<boolean>;
|
|
@@ -1,22 +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
|
-
async function isSilentRoutingCancellation(archive, context, turnId) {
|
|
8
|
-
const principal = context.session.auth.current;
|
|
9
|
-
const attributes = principal?.attributes;
|
|
10
|
-
if (principal?.authenticator !== "segmentstream-telegram" || typeof attributes?.groupRouting !== "string" || attributes.chatType !== "group" && attributes.chatType !== "supergroup" || typeof attributes.chatId !== "string") return false;
|
|
11
|
-
const { events } = await archive.context({
|
|
12
|
-
provider: "telegram",
|
|
13
|
-
chatId: attributes.chatId,
|
|
14
|
-
...typeof attributes.messageThreadId === "string" ? { threadId: attributes.messageThreadId } : {},
|
|
15
|
-
limit: 200,
|
|
16
|
-
includeDecisions: true,
|
|
17
|
-
...typeof attributes.replyToMessageId === "string" ? { messageId: attributes.replyToMessageId } : {}
|
|
18
|
-
});
|
|
19
|
-
const scoped = events.filter((event) => event.provider === "telegram" && event.chatId === attributes.chatId && event.threadId === (attributes.messageThreadId ?? null) && event.sessionId === context.session.id && event.turnId === turnId && event.kind === "decision");
|
|
20
|
-
return scoped.some((event) => event.decision === "silent" && (event.reason === "burst_already_answered" || event.reason === "dequeue_observe")) || !scoped.some((event) => event.decision === "reply" && (event.reason === "burst_turn_started" || event.reason === "dequeue_reply"));
|
|
21
|
-
}
|
|
22
|
-
export { isSilentRoutingCancellation };
|
|
@@ -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 };
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { HookContext, HookEvent } from "eve/hooks";
|
|
2
|
-
import { type Conversations } from "../conversations.js";
|
|
3
|
-
import { type ConversationDecideFunction } from "./conversation-router.js";
|
|
4
|
-
/** Recheck queued callers at the durable turn boundary, before any model or business action. */
|
|
5
|
-
export declare function conversationDequeueGuard(archive?: Conversations, classifier?: ConversationDecideFunction): (event: HookEvent<"turn.started">, ctx: HookContext) => Promise<void>;
|
|
@@ -1,145 +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
|
-
import { conversationEvent, conversations } from "../conversations.mjs";
|
|
8
|
-
import { addressingReason } from "./signals.mjs";
|
|
9
|
-
import { recordedMessageSignals } from "./conversation.mjs";
|
|
10
|
-
import { conversationDecisionRouter } from "./conversation-router.mjs";
|
|
11
|
-
import { routeMessage } from "./router.mjs";
|
|
12
|
-
import { archivedRoutingMessages } from "./routing-context.mjs";
|
|
13
|
-
import { routingBurst } from "./burst.mjs";
|
|
14
|
-
import { conversationCoordinates } from "../conversation-runtime.mjs";
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
-
import { decide } from "eve/ai";
|
|
17
|
-
function conversationDequeueGuard(archive = conversations(), classifier = decide) {
|
|
18
|
-
return async (event, ctx) => {
|
|
19
|
-
const auth = ctx.session.auth.current;
|
|
20
|
-
const encoded = auth?.attributes.groupRouting;
|
|
21
|
-
const coordinates = conversationCoordinates(ctx);
|
|
22
|
-
if (auth?.authenticator !== "segmentstream-telegram" || typeof encoded !== "string" || coordinates?.chatType !== "group") return;
|
|
23
|
-
try {
|
|
24
|
-
const input = JSON.parse(encoded);
|
|
25
|
-
if (input.policy.smart.engagement.policy !== "conversation") return;
|
|
26
|
-
if (input.signals.sender.externalId !== auth.subject || input.signals.conversation.externalId !== coordinates.chatId) throw new Error("Routing provenance does not match the turn caller.");
|
|
27
|
-
const events = (await archive.context({
|
|
28
|
-
provider: coordinates.provider,
|
|
29
|
-
chatId: coordinates.chatId,
|
|
30
|
-
...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
|
|
31
|
-
limit: 200,
|
|
32
|
-
includeDecisions: true
|
|
33
|
-
})).events;
|
|
34
|
-
const fence = `group-routing:burst:${createHash("sha256").update(input.burst_id).digest("hex")}`;
|
|
35
|
-
const existing = events.find((entry) => entry.eventId === fence);
|
|
36
|
-
if (existing !== void 0) {
|
|
37
|
-
if (existing.turnId !== ctx.session.turn.id) {
|
|
38
|
-
await archive.append(conversationEvent({
|
|
39
|
-
...coordinates,
|
|
40
|
-
eventId: `${fence}:duplicate:${ctx.session.turn.id}`,
|
|
41
|
-
sessionId: ctx.session.id,
|
|
42
|
-
turnId: ctx.session.turn.id,
|
|
43
|
-
messageId: input.signals.message.id,
|
|
44
|
-
kind: "decision",
|
|
45
|
-
decision: "silent",
|
|
46
|
-
reason: "burst_already_answered",
|
|
47
|
-
argsSummary: JSON.stringify({ burst_id: input.burst_id })
|
|
48
|
-
}));
|
|
49
|
-
ctx.cancel();
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const recent = archivedRoutingMessages(events, 100);
|
|
54
|
-
const decisions = [];
|
|
55
|
-
const burst = routingBurst(input.signals, recent, input.policy);
|
|
56
|
-
const latest = burst.messages.at(-1);
|
|
57
|
-
const signals = recordedMessageSignals(latest, input.signals);
|
|
58
|
-
const claimedAddressing = addressingReason(input.signals);
|
|
59
|
-
const explicit = claimedAddressing === "mention" || claimedAddressing === "command";
|
|
60
|
-
const route = explicit ? "reply" : await routeMessage({
|
|
61
|
-
groups: input.policy,
|
|
62
|
-
signals,
|
|
63
|
-
policy: input.policy.mode,
|
|
64
|
-
chatId: coordinates.chatId,
|
|
65
|
-
memberId: input.signals.sender.externalId,
|
|
66
|
-
addressed: void 0,
|
|
67
|
-
pendingQuestionAt: input.pendingQuestionAt,
|
|
68
|
-
question: async () => ({
|
|
69
|
-
groups: input.policy,
|
|
70
|
-
signals,
|
|
71
|
-
agent: input.agent,
|
|
72
|
-
processingAt: ctx.now.toISOString(),
|
|
73
|
-
agentIdentity: input.agentIdentity,
|
|
74
|
-
chat: input.chat,
|
|
75
|
-
recent,
|
|
76
|
-
burstId: input.burst_id,
|
|
77
|
-
messagesToClassify: burst.messages,
|
|
78
|
-
pendingQuestions: input.pendingQuestions,
|
|
79
|
-
message: {
|
|
80
|
-
...input.message,
|
|
81
|
-
...latest
|
|
82
|
-
}
|
|
83
|
-
}),
|
|
84
|
-
decide: conversationDecisionRouter(classifier),
|
|
85
|
-
record: (d) => decisions.push(d)
|
|
86
|
-
});
|
|
87
|
-
const base = {
|
|
88
|
-
...coordinates,
|
|
89
|
-
sessionId: ctx.session.id,
|
|
90
|
-
turnId: ctx.session.turn.id,
|
|
91
|
-
at: ctx.now.toISOString(),
|
|
92
|
-
messageId: input.signals.message.id
|
|
93
|
-
};
|
|
94
|
-
if (route === "reply") {
|
|
95
|
-
if (await archive.append(conversationEvent({
|
|
96
|
-
...base,
|
|
97
|
-
eventId: fence,
|
|
98
|
-
kind: "decision",
|
|
99
|
-
decision: "reply",
|
|
100
|
-
reason: "burst_turn_started",
|
|
101
|
-
argsSummary: JSON.stringify({ burst_id: input.burst_id })
|
|
102
|
-
})) !== true) {
|
|
103
|
-
if (!(await archive.context({
|
|
104
|
-
provider: coordinates.provider,
|
|
105
|
-
chatId: coordinates.chatId,
|
|
106
|
-
...coordinates.threadId === null ? {} : { threadId: coordinates.threadId },
|
|
107
|
-
limit: 200,
|
|
108
|
-
includeDecisions: true
|
|
109
|
-
})).events.find((e) => e.eventId === fence && e.turnId === ctx.session.turn.id)) {
|
|
110
|
-
await archive.append(conversationEvent({
|
|
111
|
-
...base,
|
|
112
|
-
eventId: `${fence}:duplicate:${ctx.session.turn.id}`,
|
|
113
|
-
kind: "decision",
|
|
114
|
-
decision: "silent",
|
|
115
|
-
reason: "burst_already_answered",
|
|
116
|
-
argsSummary: JSON.stringify({ burst_id: input.burst_id })
|
|
117
|
-
}));
|
|
118
|
-
ctx.cancel();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
await archive.append(conversationEvent({
|
|
124
|
-
...base,
|
|
125
|
-
eventId: `group-routing:dequeue:${ctx.session.turn.id}`,
|
|
126
|
-
kind: "decision",
|
|
127
|
-
decision: route === "reply" ? "reply" : "silent",
|
|
128
|
-
reason: explicit ? "dequeue_explicit_kept" : `dequeue_${route}`,
|
|
129
|
-
argsSummary: JSON.stringify({
|
|
130
|
-
router: decisions.at(-1)?.router,
|
|
131
|
-
burst_id: input.burst_id,
|
|
132
|
-
claimed_addressing: explicit ? claimedAddressing : void 0
|
|
133
|
-
})
|
|
134
|
-
}));
|
|
135
|
-
if (route === "observe") ctx.cancel();
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error("[segmentstream] conversation dequeue routing failed", {
|
|
138
|
-
error,
|
|
139
|
-
sessionId: ctx.session.id
|
|
140
|
-
});
|
|
141
|
-
ctx.cancel();
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
export { conversationDequeueGuard };
|