@segmentstream/agent 0.3.0 → 0.4.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 +18 -3
- package/dist/extension/_manifest.json +4 -1
- package/dist/extension/hooks/conversations.d.ts +2 -0
- package/dist/extension/hooks/conversations.mjs +10 -0
- package/dist/extension/instructions/conversations.d.ts +3 -0
- package/dist/extension/instructions/conversations.mjs +20 -0
- package/dist/extension/instructions.md +21 -6
- package/dist/extension/lib/confirmations.d.ts +26 -0
- package/dist/extension/lib/confirmations.mjs +96 -0
- package/dist/extension/lib/conversation-runtime.d.ts +19 -0
- package/dist/extension/lib/conversation-runtime.mjs +104 -0
- package/dist/extension/lib/conversations.d.ts +65 -0
- package/dist/extension/lib/conversations.mjs +107 -0
- package/dist/extension/lib/ontology-connection.d.ts +1 -0
- package/dist/extension/lib/ontology-connection.mjs +3 -0
- package/dist/extension/lib/ontology-tools.d.ts +11 -0
- package/dist/extension/lib/ontology-tools.mjs +41 -0
- package/dist/extension/lib/questions.d.ts +14 -0
- package/dist/extension/lib/questions.mjs +30 -0
- package/dist/extension/lib/telegram/adapter.d.ts +3 -1
- package/dist/extension/lib/telegram/adapter.mjs +63 -12
- package/dist/extension/lib/telegram/confirmations.d.ts +18 -0
- package/dist/extension/lib/telegram/confirmations.mjs +280 -0
- package/dist/extension/lib/telegram/conversations.d.ts +30 -0
- package/dist/extension/lib/telegram/conversations.mjs +287 -0
- package/dist/extension/lib/telegram/inbound-request.d.ts +4 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
- package/dist/extension/lib/telegram/inbound.d.ts +3 -0
- package/dist/extension/lib/telegram/inbound.mjs +63 -13
- package/dist/extension/lib/telegram/principal.mjs +1 -0
- package/dist/extension/lib/telegram/requests.mjs +3 -1
- package/dist/extension/lib/telegram/router.d.ts +1 -1
- package/dist/extension/lib/telegram/routing-context.mjs +2 -2
- package/dist/extension/lib/telegram/sessions.d.ts +4 -7
- package/dist/extension/tools/attachments_promote.mjs +3 -0
- package/dist/extension/tools/questions_ask.d.ts +12 -0
- package/dist/extension/tools/questions_ask.mjs +50 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.mjs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
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 { defineState } from "eve/context";
|
|
8
|
+
const pendingQuestions = defineState("segmentstream.pending-questions", () => ({}));
|
|
9
|
+
function questionKey(question) {
|
|
10
|
+
return JSON.stringify([
|
|
11
|
+
question.memberId,
|
|
12
|
+
question.chatId,
|
|
13
|
+
question.messageThreadId
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
function isPendingQuestion(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && [
|
|
18
|
+
"id",
|
|
19
|
+
"question",
|
|
20
|
+
"task",
|
|
21
|
+
"memberId",
|
|
22
|
+
"chatId",
|
|
23
|
+
"messageThreadId",
|
|
24
|
+
"address"
|
|
25
|
+
].every((key) => typeof value[key] === "string");
|
|
26
|
+
}
|
|
27
|
+
function answerMarker(id) {
|
|
28
|
+
return `[Answer to pending question ${id}]`;
|
|
29
|
+
}
|
|
30
|
+
export { answerMarker, isPendingQuestion, pendingQuestions, questionKey };
|
|
@@ -2,6 +2,7 @@ import { type TelegramChannel } from "eve/channels/telegram";
|
|
|
2
2
|
import type { SessionSettings, TelegramSettings } from "../../config.js";
|
|
3
3
|
import { type AgentAttachments } from "../attachments.js";
|
|
4
4
|
import { type Environment } from "../environment.js";
|
|
5
|
+
import { type Conversations } from "../conversations.js";
|
|
5
6
|
import { type RuntimeTokenVerifier } from "../runtime-identity.js";
|
|
6
7
|
import { type RuntimeToken } from "../runtime-token.js";
|
|
7
8
|
import { type ObservedMessages } from "./observed.js";
|
|
@@ -31,10 +32,11 @@ export interface TelegramAdapterOptions {
|
|
|
31
32
|
readonly observed?: ObservedMessages;
|
|
32
33
|
/** Verifies callers of the messages route; tests pass one on a local key set. */
|
|
33
34
|
readonly verify?: RuntimeTokenVerifier;
|
|
34
|
-
/** Reads
|
|
35
|
+
/** Reads session events for routing, policy and interactions; tests pass a fake. */
|
|
35
36
|
readonly history?: HistoryReader;
|
|
36
37
|
/** The attachment store; tests pass a fake. */
|
|
37
38
|
readonly attachments?: AgentAttachments;
|
|
39
|
+
readonly conversations?: Conversations;
|
|
38
40
|
}
|
|
39
41
|
/** The Telegram channel of the extension. */
|
|
40
42
|
export declare function segmentstreamTelegram(options: TelegramAdapterOptions): TelegramChannel;
|
|
@@ -9,16 +9,19 @@ import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs
|
|
|
9
9
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
10
10
|
import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
11
11
|
import { agentIdentities } from "../identities.mjs";
|
|
12
|
+
import { conversations } from "../conversations.mjs";
|
|
13
|
+
import { observedMessages } from "./observed.mjs";
|
|
14
|
+
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
15
|
+
import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
|
|
12
16
|
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
13
17
|
import { botIdentityCache } from "./bot.mjs";
|
|
18
|
+
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
19
|
+
import { confirmationEvents, handleConfirmationCallback } from "./confirmations.mjs";
|
|
14
20
|
import { agentChats } from "./chats.mjs";
|
|
15
|
-
import { observedMessages } from "./observed.mjs";
|
|
16
21
|
import { decisionRouter } from "./router.mjs";
|
|
17
22
|
import { replyDelivery } from "./silent.mjs";
|
|
18
|
-
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
19
23
|
import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
|
|
20
24
|
import { handleInboundMessage } from "./inbound.mjs";
|
|
21
|
-
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
22
25
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
23
26
|
import { messagesRoute } from "./requests.mjs";
|
|
24
27
|
import { withTurnErrorLogging } from "./turn-errors.mjs";
|
|
@@ -40,11 +43,27 @@ const eveHistory = async (sessionId) => {
|
|
|
40
43
|
});
|
|
41
44
|
return events;
|
|
42
45
|
};
|
|
46
|
+
const eveInteractions = async (sessionId) => {
|
|
47
|
+
const { sessions } = await import("eve/server");
|
|
48
|
+
const events = [];
|
|
49
|
+
for await (const event of sessions.attach(sessionId).stream({ follow: false })) events.push({
|
|
50
|
+
type: event.type,
|
|
51
|
+
meta: { at: event.meta.at },
|
|
52
|
+
data: event.data
|
|
53
|
+
});
|
|
54
|
+
return events;
|
|
55
|
+
};
|
|
43
56
|
function segmentstreamTelegram(options) {
|
|
44
57
|
const environment = options.environment ?? process.env;
|
|
45
58
|
const token = options.token ?? runtimeToken;
|
|
46
59
|
const observed = options.observed ?? observedMessages();
|
|
47
60
|
const readHistory = options.history ?? eveHistory;
|
|
61
|
+
const readInteractions = options.history ?? eveInteractions;
|
|
62
|
+
const archive = options.conversations ?? conversations({
|
|
63
|
+
environment,
|
|
64
|
+
token,
|
|
65
|
+
fetch: options.fetch
|
|
66
|
+
});
|
|
48
67
|
const policy = sessionPolicy({
|
|
49
68
|
settings: options.sessions,
|
|
50
69
|
read: readHistory
|
|
@@ -81,6 +100,13 @@ function segmentstreamTelegram(options) {
|
|
|
81
100
|
return identities;
|
|
82
101
|
}
|
|
83
102
|
const chats = agentChats({ identities: pairing });
|
|
103
|
+
const capture = telegramConversationCapture({
|
|
104
|
+
archive,
|
|
105
|
+
identities: pairing,
|
|
106
|
+
observed,
|
|
107
|
+
chats,
|
|
108
|
+
verify: telegramWebhookVerifier(() => webhookSecret(environment))
|
|
109
|
+
});
|
|
84
110
|
let store = options.attachments;
|
|
85
111
|
function attachments() {
|
|
86
112
|
store ??= agentAttachments({
|
|
@@ -94,7 +120,7 @@ function segmentstreamTelegram(options) {
|
|
|
94
120
|
api: {
|
|
95
121
|
apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
96
122
|
fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
97
|
-
fetch: (input, init) => telegramProxyFetch(proxy())(input, init)
|
|
123
|
+
fetch: (input, init) => captureTelegramFetch(telegramProxyFetch(proxy()), archive, pairing)(input, init)
|
|
98
124
|
},
|
|
99
125
|
credentials: {
|
|
100
126
|
botToken: TELEGRAM_PLACEHOLDER_TOKEN,
|
|
@@ -110,12 +136,12 @@ function segmentstreamTelegram(options) {
|
|
|
110
136
|
sessions: options.sessions,
|
|
111
137
|
sessionPolicy: policy,
|
|
112
138
|
sttModel: options.models().stt,
|
|
113
|
-
identities: pairing(),
|
|
139
|
+
identities: conversationIdentities(pairing()),
|
|
114
140
|
bot,
|
|
115
|
-
decide: decisionRouter({
|
|
141
|
+
decide: captureRouteDecider(decisionRouter({
|
|
116
142
|
decide: options.decide,
|
|
117
143
|
model: options.models().decision
|
|
118
|
-
}),
|
|
144
|
+
})),
|
|
119
145
|
transcribe: options.transcribe ?? gatewayTranscriber,
|
|
120
146
|
download: (fileId) => downloadTelegramFile({
|
|
121
147
|
proxy: proxy(),
|
|
@@ -124,11 +150,22 @@ function segmentstreamTelegram(options) {
|
|
|
124
150
|
attachments: attachments(),
|
|
125
151
|
observed,
|
|
126
152
|
readHistory,
|
|
153
|
+
history: readInteractions,
|
|
127
154
|
chats,
|
|
128
155
|
connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
|
|
129
156
|
}
|
|
130
157
|
}),
|
|
131
|
-
|
|
158
|
+
onCallbackQuery: (context, query) => handleConfirmationCallback({
|
|
159
|
+
context,
|
|
160
|
+
query,
|
|
161
|
+
identities: pairing(),
|
|
162
|
+
read: readInteractions,
|
|
163
|
+
scope: options.sessions().scope
|
|
164
|
+
}),
|
|
165
|
+
events: {
|
|
166
|
+
...confirmationEvents,
|
|
167
|
+
"message.completed": replyDelivery(observed)
|
|
168
|
+
}
|
|
132
169
|
});
|
|
133
170
|
const adapter = channel.adapter;
|
|
134
171
|
const fetchTelegramFile = adapter?.fetchFile;
|
|
@@ -158,7 +195,8 @@ function segmentstreamTelegram(options) {
|
|
|
158
195
|
routes: [...channel.routes.map((route) => gatedRoute({
|
|
159
196
|
route,
|
|
160
197
|
options,
|
|
161
|
-
environment
|
|
198
|
+
environment,
|
|
199
|
+
capture
|
|
162
200
|
})), messagesRoute({
|
|
163
201
|
channel,
|
|
164
202
|
enabled: () => channelsEnabled(environment),
|
|
@@ -188,9 +226,22 @@ function gatedRoute(input) {
|
|
|
188
226
|
from: args.from
|
|
189
227
|
};
|
|
190
228
|
inbound.waitUntil = args.waitUntil;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
229
|
+
inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
|
|
230
|
+
requestId,
|
|
231
|
+
optionId: decision
|
|
232
|
+
}], { auth });
|
|
233
|
+
return runInboundRequest(inbound, () => input.capture(request, async () => {
|
|
234
|
+
const tasks = [];
|
|
235
|
+
const response = await route.handler(request, {
|
|
236
|
+
...args,
|
|
237
|
+
waitUntil: (task) => {
|
|
238
|
+
tasks.push(task);
|
|
239
|
+
args.waitUntil(task);
|
|
240
|
+
},
|
|
241
|
+
from: (address) => withReplacedText(args.from(inbound.address ?? scoped(address)), inbound)
|
|
242
|
+
});
|
|
243
|
+
await Promise.all(tasks);
|
|
244
|
+
return response;
|
|
194
245
|
}));
|
|
195
246
|
}
|
|
196
247
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChannelEvents } from "eve/channels";
|
|
2
|
+
import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
|
|
3
|
+
import type { AgentIdentities } from "../identities.js";
|
|
4
|
+
import { type PendingQuestion } from "../questions.js";
|
|
5
|
+
import { type HistoryReader, type RecordedEvent } from "./sessions.js";
|
|
6
|
+
export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
|
|
7
|
+
export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
|
|
8
|
+
export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
|
|
9
|
+
/** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
|
|
10
|
+
export declare function handleConfirmationCallback(input: {
|
|
11
|
+
readonly context: TelegramContext;
|
|
12
|
+
readonly query: TelegramCallbackQuery;
|
|
13
|
+
readonly identities: AgentIdentities;
|
|
14
|
+
readonly read: HistoryReader;
|
|
15
|
+
readonly scope: "chat" | "thread";
|
|
16
|
+
readonly now?: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
|
|
@@ -0,0 +1,280 @@
|
|
|
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 { memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
|
+
import { confirmations } from "../confirmations.mjs";
|
|
9
|
+
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
|
+
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
11
|
+
import { formatAddress, scopedAddress } from "./sessions.mjs";
|
|
12
|
+
import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
const PREFIX = "ss-confirm:";
|
|
15
|
+
function shortHash(value) {
|
|
16
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 20);
|
|
17
|
+
}
|
|
18
|
+
function confirmationCallback(requestId, memberId, decision) {
|
|
19
|
+
return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${decision === "approve" ? "y" : "n"}`;
|
|
20
|
+
}
|
|
21
|
+
function object(value) {
|
|
22
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
23
|
+
}
|
|
24
|
+
function openConfirmations(events) {
|
|
25
|
+
const open = new Map();
|
|
26
|
+
for (const event of events) {
|
|
27
|
+
const data = object(event.data);
|
|
28
|
+
if (event.type === "context.cleared") open.clear();
|
|
29
|
+
if (event.type === "input.requested" && Array.isArray(data.requests)) for (const item of data.requests) {
|
|
30
|
+
const request = object(item);
|
|
31
|
+
const action = object(request.action);
|
|
32
|
+
if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
|
|
33
|
+
requestId: request.requestId,
|
|
34
|
+
callId: action.callId,
|
|
35
|
+
at: Date.parse(event.meta.at)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
|
|
39
|
+
const id = object(item).requestId;
|
|
40
|
+
if (typeof id === "string") open.delete(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...open.values()];
|
|
44
|
+
}
|
|
45
|
+
function questionFromHistory(events, owner) {
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
const data = object(event.data);
|
|
49
|
+
if (event.type === "context.cleared") pending.clear();
|
|
50
|
+
if (event.type === "action.result") {
|
|
51
|
+
const result = object(data.result);
|
|
52
|
+
const candidate = object(result.output).pendingQuestion;
|
|
53
|
+
if (typeof result.toolName === "string" && result.toolName.endsWith("__questions_ask") && isPendingQuestion(candidate)) pending.set(questionKey(candidate), candidate);
|
|
54
|
+
}
|
|
55
|
+
if (event.type === "message.received" && typeof data.message === "string" && data.message.startsWith("[Answer to pending question ")) {
|
|
56
|
+
for (const [key, question] of pending) if (data.message.startsWith(answerMarker(question.id))) pending.delete(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
|
|
60
|
+
}
|
|
61
|
+
function questionAddress(owner) {
|
|
62
|
+
return formatAddress({
|
|
63
|
+
...owner,
|
|
64
|
+
conversationId: `question-${shortHash(owner.memberId)}`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function handleConfirmationCallback(input) {
|
|
68
|
+
const { context, query } = input;
|
|
69
|
+
const notice = (text) => context.telegram.answerCallbackQuery({
|
|
70
|
+
callbackQueryId: query.id,
|
|
71
|
+
text
|
|
72
|
+
});
|
|
73
|
+
if (!query.data?.startsWith(PREFIX) || query.message === void 0 || query.from.isBot) {
|
|
74
|
+
await notice("Unsupported action.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const [requestHash, memberHash, choice] = query.data.slice(11).split(":");
|
|
78
|
+
const lookup = await input.identities.lookup({
|
|
79
|
+
provider: "telegram",
|
|
80
|
+
externalId: query.from.id
|
|
81
|
+
});
|
|
82
|
+
if (lookup.status !== "paired" || shortHash(lookup.member.id) !== memberHash) {
|
|
83
|
+
await notice("Only the requesting member can confirm or cancel this action.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const address = scopedAddress(formatAddress({
|
|
87
|
+
chatId: query.message.chat.id,
|
|
88
|
+
messageThreadId: String(query.message.messageThreadId ?? ""),
|
|
89
|
+
conversationId: query.message.chat.type === "private" ? "" : query.message.messageId
|
|
90
|
+
}), input.scope);
|
|
91
|
+
const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
|
|
92
|
+
const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
|
|
93
|
+
if (prompt === void 0 || choice !== "y" && choice !== "n") {
|
|
94
|
+
await notice("This confirmation is no longer pending.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
|
|
98
|
+
const decision = expired || choice === "n" ? "cancel" : "approve";
|
|
99
|
+
const principal = telegramPrincipal({
|
|
100
|
+
member: lookup.member,
|
|
101
|
+
message: {
|
|
102
|
+
...query.message,
|
|
103
|
+
from: query.from,
|
|
104
|
+
text: "",
|
|
105
|
+
caption: "",
|
|
106
|
+
raw: {},
|
|
107
|
+
attachments: []
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const source = currentInboundRequest()?.respond;
|
|
111
|
+
if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
|
|
112
|
+
await source(address, prompt.requestId, decision, {
|
|
113
|
+
...principal,
|
|
114
|
+
attributes: {
|
|
115
|
+
...principal.attributes,
|
|
116
|
+
confirmationRequestId: prompt.requestId,
|
|
117
|
+
confirmationDecision: decision
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
|
|
121
|
+
}
|
|
122
|
+
const confirmationEvents = {
|
|
123
|
+
"context.cleared"(_data, channel) {
|
|
124
|
+
confirmations.update(() => ({}));
|
|
125
|
+
pendingQuestions.update(() => ({}));
|
|
126
|
+
const state = channel.state;
|
|
127
|
+
state.confirmationMessages = {};
|
|
128
|
+
state.pendingQuestions = {};
|
|
129
|
+
},
|
|
130
|
+
async "input.requested"(data, channel, ctx) {
|
|
131
|
+
for (const request of data.requests) {
|
|
132
|
+
if (request.kind !== "tool-approval") {
|
|
133
|
+
const rendered = renderTelegramInputRequest(request, channel.state);
|
|
134
|
+
const sent = await channel.telegram.post({
|
|
135
|
+
text: rendered.text,
|
|
136
|
+
reply_markup: rendered.replyMarkup
|
|
137
|
+
});
|
|
138
|
+
if (rendered.freeformRequestId !== void 0) registerTelegramFreeformPrompt(channel.state, {
|
|
139
|
+
messageId: sent.id,
|
|
140
|
+
requestId: request.requestId
|
|
141
|
+
});
|
|
142
|
+
else channel.state.hitlPrompts = {
|
|
143
|
+
...channel.state.hitlPrompts,
|
|
144
|
+
[request.requestId]: {
|
|
145
|
+
messageId: sent.id,
|
|
146
|
+
text: rendered.text,
|
|
147
|
+
options: (request.options ?? []).map(({ id, label }) => ({
|
|
148
|
+
id,
|
|
149
|
+
label
|
|
150
|
+
}))
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
|
|
156
|
+
if (memberId === void 0) throw new Error("Confirmation requires a member.");
|
|
157
|
+
const text = `Confirm ${request.action.toolName}?\n${JSON.stringify(request.action.input, null, 2)}`.slice(0, 3900);
|
|
158
|
+
const sent = await channel.telegram.post({
|
|
159
|
+
text,
|
|
160
|
+
reply_markup: { inline_keyboard: [[{
|
|
161
|
+
text: "Confirm",
|
|
162
|
+
callback_data: confirmationCallback(request.requestId, memberId, "approve")
|
|
163
|
+
}, {
|
|
164
|
+
text: "Cancel",
|
|
165
|
+
callback_data: confirmationCallback(request.requestId, memberId, "cancel")
|
|
166
|
+
}]] }
|
|
167
|
+
});
|
|
168
|
+
const state = channel.state;
|
|
169
|
+
state.confirmationMessages = {
|
|
170
|
+
...state.confirmationMessages,
|
|
171
|
+
[request.action.callId]: {
|
|
172
|
+
requestId: request.requestId,
|
|
173
|
+
messageId: sent.id,
|
|
174
|
+
text
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async "input.resolved"(data, channel) {
|
|
180
|
+
const state = channel.state;
|
|
181
|
+
for (const resolution of data.resolutions) {
|
|
182
|
+
const entry = Object.entries(state.confirmationMessages ?? {}).find(([, message]) => message.requestId === resolution.requestId);
|
|
183
|
+
if (entry === void 0) {
|
|
184
|
+
const prompt = channel.state.hitlPrompts?.[resolution.requestId];
|
|
185
|
+
if (prompt !== void 0) {
|
|
186
|
+
const response = resolution.response;
|
|
187
|
+
const label = response?.text ?? prompt.options.find((option) => option.id === response?.optionId)?.label;
|
|
188
|
+
await channel.telegram.editMessageText({
|
|
189
|
+
messageId: prompt.messageId,
|
|
190
|
+
replyMarkup: { inline_keyboard: [] },
|
|
191
|
+
text: `${prompt.text}\n\n${label ?? resolution.outcome}`
|
|
192
|
+
});
|
|
193
|
+
delete channel.state.hitlPrompts?.[resolution.requestId];
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const [callId, message] = entry;
|
|
198
|
+
const text = resolution.outcome === "approved" ? "Confirmed. Executing…" : "Cancelled. Nothing executed.";
|
|
199
|
+
await channel.telegram.editMessageText({
|
|
200
|
+
messageId: message.messageId,
|
|
201
|
+
replyMarkup: { inline_keyboard: [] },
|
|
202
|
+
text: `${message.text}\n\n${text}`
|
|
203
|
+
});
|
|
204
|
+
if (resolution.outcome !== "approved") {
|
|
205
|
+
delete state.confirmationMessages?.[callId];
|
|
206
|
+
confirmations.update((all) => ({
|
|
207
|
+
...all,
|
|
208
|
+
...all[callId] === void 0 ? {} : { [callId]: {
|
|
209
|
+
...all[callId],
|
|
210
|
+
status: "cancelled"
|
|
211
|
+
} }
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async "action.result"(data, channel) {
|
|
217
|
+
const state = channel.state;
|
|
218
|
+
if (data.result.kind === "tool-result") {
|
|
219
|
+
const question = object(data.result.output).pendingQuestion;
|
|
220
|
+
if (data.result.toolName.endsWith("__questions_ask") && isPendingQuestion(question)) {
|
|
221
|
+
const address = channel.continuation?.token ?? formatAddress({
|
|
222
|
+
chatId: channel.telegram.chatId,
|
|
223
|
+
messageThreadId: String(channel.telegram.messageThreadId ?? ""),
|
|
224
|
+
conversationId: channel.telegram.conversationId ?? ""
|
|
225
|
+
});
|
|
226
|
+
const saved = {
|
|
227
|
+
...question,
|
|
228
|
+
address
|
|
229
|
+
};
|
|
230
|
+
pendingQuestions.update((all) => ({
|
|
231
|
+
...all,
|
|
232
|
+
[questionKey(saved)]: saved
|
|
233
|
+
}));
|
|
234
|
+
state.pendingQuestions = {
|
|
235
|
+
...state.pendingQuestions,
|
|
236
|
+
[questionKey(saved)]: saved
|
|
237
|
+
};
|
|
238
|
+
channel.continuation?.alias(formatAddress({
|
|
239
|
+
chatId: question.chatId,
|
|
240
|
+
messageThreadId: question.messageThreadId,
|
|
241
|
+
conversationId: ""
|
|
242
|
+
}));
|
|
243
|
+
channel.continuation?.alias(questionAddress(saved));
|
|
244
|
+
await channel.telegram.post({
|
|
245
|
+
text: question.question,
|
|
246
|
+
reply_markup: {
|
|
247
|
+
force_reply: true,
|
|
248
|
+
selective: true
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const message = state.confirmationMessages?.[data.result.callId];
|
|
254
|
+
if (message === void 0) return;
|
|
255
|
+
const failed = data.status !== "completed" || data.result.kind === "tool-result" && (data.result.isError === true || object(data.result.output).isError === true || object(object(data.result.output).validation).ok === false);
|
|
256
|
+
const result = data.result.kind === "tool-result" ? JSON.stringify(data.result.output) : "";
|
|
257
|
+
await channel.telegram.editMessageText({
|
|
258
|
+
messageId: message.messageId,
|
|
259
|
+
replyMarkup: { inline_keyboard: [] },
|
|
260
|
+
text: `${message.text}\n\n${failed ? "Failed" : "Completed"}: ${result}`.slice(0, 4096)
|
|
261
|
+
});
|
|
262
|
+
delete state.confirmationMessages?.[data.result.callId];
|
|
263
|
+
},
|
|
264
|
+
"turn.started"(_data, channel, ctx) {
|
|
265
|
+
const memberId = memberIdOf(ctx.session.auth.current);
|
|
266
|
+
const answeredId = ctx.session.auth.current?.attributes.pendingQuestionId;
|
|
267
|
+
if (memberId !== void 0 && typeof answeredId === "string") {
|
|
268
|
+
const key = questionKey({
|
|
269
|
+
memberId,
|
|
270
|
+
chatId: channel.telegram.chatId,
|
|
271
|
+
messageThreadId: String(channel.telegram.messageThreadId ?? "")
|
|
272
|
+
});
|
|
273
|
+
if (pendingQuestions.get()[key]?.id === answeredId) {
|
|
274
|
+
pendingQuestions.update((all) => Object.fromEntries(Object.entries(all).filter(([name]) => name !== key)));
|
|
275
|
+
delete channel.state.pendingQuestions?.[key];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
export { confirmationCallback, confirmationEvents, handleConfirmationCallback, questionAddress, questionFromHistory };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentIdentities } from "../identities.js";
|
|
2
|
+
import { type Conversations } from "../conversations.js";
|
|
3
|
+
import type { AgentChats } from "./chats.js";
|
|
4
|
+
import type { ObservedMessages } from "./observed.js";
|
|
5
|
+
import type { AddressingReason, RouteDecider } from "./router.js";
|
|
6
|
+
/** Runs after webhook verification, before eve drops bot/channel/service updates. */
|
|
7
|
+
export declare function captureTelegramWebhook(input: {
|
|
8
|
+
readonly request: Request;
|
|
9
|
+
readonly archive: Conversations;
|
|
10
|
+
readonly identities: AgentIdentities;
|
|
11
|
+
readonly observed: ObservedMessages;
|
|
12
|
+
readonly chats?: AgentChats;
|
|
13
|
+
readonly next: () => Promise<Response>;
|
|
14
|
+
}): Promise<Response>;
|
|
15
|
+
/** Additive inbound hook: the router keeps ownership of the reply policy. */
|
|
16
|
+
export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
|
|
17
|
+
export declare function recordRouteDecision(route: "reply" | "observe", addressed: boolean | AddressingReason | undefined, policy: string): void;
|
|
18
|
+
export declare function captureRouteDecider(decide: RouteDecider): RouteDecider;
|
|
19
|
+
/** Captures every Telegram sendMessage chunk, including pairing/HITL/proactive replies. */
|
|
20
|
+
export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
|
|
21
|
+
/** Reuses the request's verified lookup; it never survives into another inbound request. */
|
|
22
|
+
export declare function conversationIdentities(identities: AgentIdentities): AgentIdentities;
|
|
23
|
+
/** Verification and failure acknowledgement stay outside eve's swallowed onMessage failures. */
|
|
24
|
+
export declare function telegramConversationCapture(input: {
|
|
25
|
+
readonly archive: Conversations;
|
|
26
|
+
readonly identities: () => AgentIdentities;
|
|
27
|
+
readonly observed: ObservedMessages;
|
|
28
|
+
readonly chats: AgentChats;
|
|
29
|
+
readonly verify: (request: Request) => boolean;
|
|
30
|
+
}): (request: Request, next: () => Promise<Response>) => Promise<Response>;
|