@segmentstream/agent 0.2.1 → 0.3.1
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 +8 -2
- package/dist/extension/_manifest.json +2 -1
- package/dist/extension/instructions.md +29 -7
- package/dist/extension/lib/attachment-formats.d.ts +16 -0
- package/dist/extension/lib/attachment-formats.mjs +238 -0
- package/dist/extension/lib/attachment-query.d.ts +31 -0
- package/dist/extension/lib/attachment-query.mjs +125 -0
- package/dist/extension/lib/attachment-tables.d.ts +33 -0
- package/dist/extension/lib/attachment-tables.mjs +186 -0
- package/dist/extension/lib/attachment-tools.d.ts +34 -3
- package/dist/extension/lib/attachment-tools.mjs +139 -30
- package/dist/extension/lib/confirmations.d.ts +26 -0
- package/dist/extension/lib/confirmations.mjs +96 -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 +1 -1
- package/dist/extension/lib/telegram/adapter.mjs +31 -4
- package/dist/extension/lib/telegram/confirmations.d.ts +18 -0
- package/dist/extension/lib/telegram/confirmations.mjs +280 -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 +50 -9
- 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/attachments_read.mjs +47 -5
- package/dist/extension/tools/attachments_table.d.ts +64 -0
- package/dist/extension/tools/attachments_table.mjs +198 -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 +2 -0
- package/dist/tools/index.mjs +3 -1
- package/package.json +9 -2
|
@@ -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 };
|
|
@@ -31,7 +31,7 @@ export interface TelegramAdapterOptions {
|
|
|
31
31
|
readonly observed?: ObservedMessages;
|
|
32
32
|
/** Verifies callers of the messages route; tests pass one on a local key set. */
|
|
33
33
|
readonly verify?: RuntimeTokenVerifier;
|
|
34
|
-
/** Reads
|
|
34
|
+
/** Reads session events for routing, policy and interactions; tests pass a fake. */
|
|
35
35
|
readonly history?: HistoryReader;
|
|
36
36
|
/** The attachment store; tests pass a fake. */
|
|
37
37
|
readonly attachments?: AgentAttachments;
|
|
@@ -11,14 +11,15 @@ import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
|
11
11
|
import { agentIdentities } from "../identities.mjs";
|
|
12
12
|
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
13
13
|
import { botIdentityCache } from "./bot.mjs";
|
|
14
|
+
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
15
|
+
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
16
|
+
import { confirmationEvents, handleConfirmationCallback } from "./confirmations.mjs";
|
|
14
17
|
import { agentChats } from "./chats.mjs";
|
|
15
18
|
import { observedMessages } from "./observed.mjs";
|
|
16
19
|
import { decisionRouter } from "./router.mjs";
|
|
17
20
|
import { replyDelivery } from "./silent.mjs";
|
|
18
|
-
import { scopedAddress, sessionPolicy } from "./sessions.mjs";
|
|
19
21
|
import { downloadTelegramFile, gatewayTranscriber } from "./voice.mjs";
|
|
20
22
|
import { handleInboundMessage } from "./inbound.mjs";
|
|
21
|
-
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
22
23
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
23
24
|
import { messagesRoute } from "./requests.mjs";
|
|
24
25
|
import { withTurnErrorLogging } from "./turn-errors.mjs";
|
|
@@ -40,11 +41,22 @@ const eveHistory = async (sessionId) => {
|
|
|
40
41
|
});
|
|
41
42
|
return events;
|
|
42
43
|
};
|
|
44
|
+
const eveInteractions = async (sessionId) => {
|
|
45
|
+
const { sessions } = await import("eve/server");
|
|
46
|
+
const events = [];
|
|
47
|
+
for await (const event of sessions.attach(sessionId).stream({ follow: false })) events.push({
|
|
48
|
+
type: event.type,
|
|
49
|
+
meta: { at: event.meta.at },
|
|
50
|
+
data: event.data
|
|
51
|
+
});
|
|
52
|
+
return events;
|
|
53
|
+
};
|
|
43
54
|
function segmentstreamTelegram(options) {
|
|
44
55
|
const environment = options.environment ?? process.env;
|
|
45
56
|
const token = options.token ?? runtimeToken;
|
|
46
57
|
const observed = options.observed ?? observedMessages();
|
|
47
58
|
const readHistory = options.history ?? eveHistory;
|
|
59
|
+
const readInteractions = options.history ?? eveInteractions;
|
|
48
60
|
const policy = sessionPolicy({
|
|
49
61
|
settings: options.sessions,
|
|
50
62
|
read: readHistory
|
|
@@ -124,11 +136,22 @@ function segmentstreamTelegram(options) {
|
|
|
124
136
|
attachments: attachments(),
|
|
125
137
|
observed,
|
|
126
138
|
readHistory,
|
|
139
|
+
history: readInteractions,
|
|
127
140
|
chats,
|
|
128
141
|
connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
|
|
129
142
|
}
|
|
130
143
|
}),
|
|
131
|
-
|
|
144
|
+
onCallbackQuery: (context, query) => handleConfirmationCallback({
|
|
145
|
+
context,
|
|
146
|
+
query,
|
|
147
|
+
identities: pairing(),
|
|
148
|
+
read: readInteractions,
|
|
149
|
+
scope: options.sessions().scope
|
|
150
|
+
}),
|
|
151
|
+
events: {
|
|
152
|
+
...confirmationEvents,
|
|
153
|
+
"message.completed": replyDelivery(observed)
|
|
154
|
+
}
|
|
132
155
|
});
|
|
133
156
|
const adapter = channel.adapter;
|
|
134
157
|
const fetchTelegramFile = adapter?.fetchFile;
|
|
@@ -188,9 +211,13 @@ function gatedRoute(input) {
|
|
|
188
211
|
from: args.from
|
|
189
212
|
};
|
|
190
213
|
inbound.waitUntil = args.waitUntil;
|
|
214
|
+
inbound.respond = (address, requestId, decision, auth) => args.from(scoped(address)).respond([{
|
|
215
|
+
requestId,
|
|
216
|
+
optionId: decision
|
|
217
|
+
}], { auth });
|
|
191
218
|
return runInboundRequest(inbound, () => route.handler(request, {
|
|
192
219
|
...args,
|
|
193
|
-
from: (address) => withReplacedText(args.from(scoped(address)), inbound)
|
|
220
|
+
from: (address) => withReplacedText(args.from(inbound.address ?? scoped(address)), inbound)
|
|
194
221
|
}));
|
|
195
222
|
}
|
|
196
223
|
};
|
|
@@ -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 { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
10
|
+
import { currentInboundRequest } from "./inbound-request.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 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChannelSource } from "eve/channels";
|
|
2
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
3
|
import type { Attachment } from "../attachments.js";
|
|
3
4
|
import type { SessionOperations } from "./sessions.js";
|
|
4
5
|
/** What became of one Telegram file of the message: stored as an attachment, or why not. */
|
|
@@ -32,6 +33,9 @@ export declare class InboundRequest {
|
|
|
32
33
|
sessions: SessionOperations | undefined;
|
|
33
34
|
/** Keeps work the request started, such as a chat report, alive after it answers. */
|
|
34
35
|
waitUntil: ((task: Promise<unknown>) => void) | undefined;
|
|
36
|
+
/** Continues the exact pending question's session, including under thread scope. */
|
|
37
|
+
address: string | undefined;
|
|
38
|
+
respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext) => Promise<unknown>) | undefined;
|
|
35
39
|
constructor(
|
|
36
40
|
/** The agent's description from the compiled manifest, for the routing decision. */
|
|
37
41
|
agentDescription: () => Promise<string>);
|
|
@@ -38,11 +38,14 @@ export interface InboundDependencies {
|
|
|
38
38
|
readonly identities: AgentIdentities;
|
|
39
39
|
readonly bot: BotIdentityLookup;
|
|
40
40
|
readonly decide: RouteDecider;
|
|
41
|
+
/** Full session history for open questions, which can survive compaction. */
|
|
42
|
+
readonly history?: HistoryReader;
|
|
41
43
|
readonly transcribe: Transcriber;
|
|
42
44
|
readonly download: (fileId: string) => Promise<Uint8Array>;
|
|
43
45
|
/** The attachment store; without it, files reach the turn as eve's Telegram file parts. */
|
|
44
46
|
readonly attachments?: AgentAttachments;
|
|
45
47
|
readonly observed: ObservedMessages;
|
|
48
|
+
/** Recent session events for smart routing. */
|
|
46
49
|
readonly readHistory: HistoryReader;
|
|
47
50
|
readonly chats: AgentChats;
|
|
48
51
|
/** The app's `/agent/connect` link for a pairing code. */
|
|
@@ -5,12 +5,14 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { telegramPrincipal } from "./principal.mjs";
|
|
8
|
+
import { answerMarker } from "../questions.mjs";
|
|
9
|
+
import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
|
|
10
|
+
import { questionAddress, questionFromHistory } from "./confirmations.mjs";
|
|
8
11
|
import { chatContext } from "./chats.mjs";
|
|
9
12
|
import { attachmentsContext, stageFiles } from "./files.mjs";
|
|
10
13
|
import { observedChatKey, observedContext } from "./observed.mjs";
|
|
11
14
|
import { addressingReason, routeMessage } from "./router.mjs";
|
|
12
15
|
import { routingContext } from "./routing-context.mjs";
|
|
13
|
-
import { messageAddress, scopedAddress } from "./sessions.mjs";
|
|
14
16
|
import { voiceNote, voiceOf, voiceText } from "./voice.mjs";
|
|
15
17
|
const UNKNOWN_PRIVATE = "Hi, I don't know you yet. Connect your Telegram account to the workspace and I'll act as you. The button works for ten minutes.";
|
|
16
18
|
const UNKNOWN_IN_GROUP = "I don't know you yet. Message me privately to connect your account.";
|
|
@@ -184,13 +186,32 @@ async function handleInboundMessage(input) {
|
|
|
184
186
|
});
|
|
185
187
|
return null;
|
|
186
188
|
}
|
|
187
|
-
|
|
189
|
+
let address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
|
|
188
190
|
const operations = input.request?.sessions;
|
|
191
|
+
const topicAddress = formatAddress({
|
|
192
|
+
chatId: sent.chat.id,
|
|
193
|
+
messageThreadId: String(sent.messageThreadId ?? ""),
|
|
194
|
+
conversationId: ""
|
|
195
|
+
});
|
|
196
|
+
const questionOwner = {
|
|
197
|
+
memberId: lookup.member.id,
|
|
198
|
+
chatId: sent.chat.id,
|
|
199
|
+
messageThreadId: String(sent.messageThreadId ?? "")
|
|
200
|
+
};
|
|
201
|
+
const memberQuestionAddress = questionAddress(questionOwner);
|
|
202
|
+
const questionSession = await operations?.resolveSession(memberQuestionAddress);
|
|
203
|
+
const owner = questionSession ?? await operations?.resolveSession(address) ?? await operations?.resolveSession(topicAddress);
|
|
204
|
+
const pending = owner === void 0 || dependencies.history === void 0 ? void 0 : questionFromHistory(await dependencies.history(owner.id), questionOwner);
|
|
205
|
+
let answering = pending !== void 0 && pending.memberId === lookup.member.id && pending.chatId === sent.chat.id && pending.messageThreadId === String(sent.messageThreadId ?? "");
|
|
206
|
+
if (answering && pending !== void 0 && input.request !== void 0) {
|
|
207
|
+
address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
|
|
208
|
+
input.request.address = address;
|
|
209
|
+
}
|
|
189
210
|
const voice = voiceOf(sent.raw);
|
|
190
211
|
const caption = sent.text || sent.caption;
|
|
191
212
|
const notes = [];
|
|
192
213
|
let text = caption;
|
|
193
|
-
if (voice !== void 0 && (addressed !== void 0 || dependencies.policy === "smart")) {
|
|
214
|
+
if (voice !== void 0 && (addressed !== void 0 || answering || dependencies.policy === "smart")) {
|
|
194
215
|
const spoken = await voiceText({
|
|
195
216
|
voice,
|
|
196
217
|
model: dependencies.sttModel,
|
|
@@ -209,7 +230,7 @@ async function handleInboundMessage(input) {
|
|
|
209
230
|
chatId: sent.chat.id,
|
|
210
231
|
memberId: lookup.member.id,
|
|
211
232
|
policy: dependencies.policy,
|
|
212
|
-
addressed,
|
|
233
|
+
addressed: addressed ?? (answering ? "pending-question" : void 0),
|
|
213
234
|
decide: dependencies.decide,
|
|
214
235
|
question: async () => {
|
|
215
236
|
const recent = await routingContext({
|
|
@@ -231,13 +252,26 @@ async function handleInboundMessage(input) {
|
|
|
231
252
|
}
|
|
232
253
|
if (voice !== void 0 && input.request !== void 0) input.request.text = text;
|
|
233
254
|
else if (voice !== void 0) notes.push(text);
|
|
255
|
+
if (answering && pending !== void 0 && input.request !== void 0) {
|
|
256
|
+
input.request.text = `${answerMarker(pending.id)}\n${text}`;
|
|
257
|
+
notes.push(`<pending_question>\nTask: ${pending.task}\nQuestion: ${pending.question}\nUse this member's answer to continue the pending task with their current permissions.
|
|
258
|
+
</pending_question>`);
|
|
259
|
+
}
|
|
234
260
|
if (operations !== void 0) {
|
|
235
261
|
const newest = dependencies.observed.list(chat).at(-1);
|
|
236
262
|
if (await dependencies.sessionPolicy.apply({
|
|
237
263
|
address,
|
|
238
264
|
operations,
|
|
239
265
|
...newest === void 0 ? {} : { activeAt: Date.parse(newest.at) || 0 }
|
|
240
|
-
}) === "cleared")
|
|
266
|
+
}) === "cleared") {
|
|
267
|
+
dependencies.observed.clear(chat);
|
|
268
|
+
if (answering && input.request !== void 0) {
|
|
269
|
+
answering = false;
|
|
270
|
+
input.request.text = text;
|
|
271
|
+
const questionNote = notes.findIndex((note) => note.startsWith("<pending_question>"));
|
|
272
|
+
if (questionNote !== -1) notes.splice(questionNote, 1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
241
275
|
dependencies.sessionPolicy.delivered(address);
|
|
242
276
|
}
|
|
243
277
|
await context.telegram.startTyping();
|
|
@@ -250,11 +284,18 @@ async function handleInboundMessage(input) {
|
|
|
250
284
|
if (input.request !== void 0 && files.size > 0) input.request.files = files;
|
|
251
285
|
await report;
|
|
252
286
|
dependencies.observed.remember(chat, observedMessage(sent, text, lookup.member));
|
|
287
|
+
const principal = telegramPrincipal({
|
|
288
|
+
message: sent,
|
|
289
|
+
member: lookup.member
|
|
290
|
+
});
|
|
253
291
|
return {
|
|
254
|
-
auth:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
292
|
+
auth: answering && pending !== void 0 ? {
|
|
293
|
+
...principal,
|
|
294
|
+
attributes: {
|
|
295
|
+
...principal.attributes,
|
|
296
|
+
pendingQuestionId: pending.id
|
|
297
|
+
}
|
|
298
|
+
} : principal,
|
|
258
299
|
context: [
|
|
259
300
|
...observedContext(dependencies.observed.take(chat)),
|
|
260
301
|
...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
|
|
@@ -18,6 +18,7 @@ function telegramPrincipal(input) {
|
|
|
18
18
|
chatType: message.chat.type
|
|
19
19
|
};
|
|
20
20
|
if (member.name !== null) attributes.memberName = member.name;
|
|
21
|
+
if (message.messageThreadId !== void 0) attributes.messageThreadId = String(message.messageThreadId);
|
|
21
22
|
if (message.from.username !== void 0) attributes.username = message.from.username;
|
|
22
23
|
return {
|
|
23
24
|
authenticator: TELEGRAM_AUTHENTICATOR,
|
|
@@ -87,7 +87,9 @@ function requestContext(request) {
|
|
|
87
87
|
const lines = [
|
|
88
88
|
"<agent_request>",
|
|
89
89
|
`A request from ${requester.name} (${requester.kind}, user_id ${requester.id}) arrived through the ontology action agent_chat.send_message; the message is the request.`,
|
|
90
|
-
|
|
90
|
+
"This turn already runs in the target chat. Your reply here is the message delivered to this chat. Do not look for or call agent_chat.send_message to deliver it, even if the ontology catalog is empty or that action is unavailable.",
|
|
91
|
+
`Write in this chat${to}, carry out the clear request, ask only for missing or ambiguous information, and handle the replies here.`,
|
|
92
|
+
"If the addressed person has a linked Telegram username, mention them with @username; otherwise address them by name without a tag. Never invent a username."
|
|
91
93
|
];
|
|
92
94
|
if (requester.kind === "service" || !requester.actAs) lines.push(`${requester.kind === "service" ? "The requester is a service, not a member" : "The requester has not paired a Telegram account with the agent"}, so this turn runs without a member and the ontology is not available in it. Each member who replies acts with their own permissions.`);
|
|
93
95
|
lines.push("</agent_request>");
|
|
@@ -8,7 +8,7 @@ import type { ObservedMessage, RecentMessage } from "./observed.js";
|
|
|
8
8
|
* policy, put to one decision on the decision model.
|
|
9
9
|
*/
|
|
10
10
|
export type Route = "observe" | "reply";
|
|
11
|
-
export type AddressingReason = "private" | "mention" | "reply-to-bot" | "command";
|
|
11
|
+
export type AddressingReason = "private" | "mention" | "reply-to-bot" | "command" | "pending-question";
|
|
12
12
|
/** The bot, as Telegram's `getMe` describes it. Unknown when `getMe` failed. */
|
|
13
13
|
export interface BotIdentity {
|
|
14
14
|
readonly id: string;
|
|
@@ -23,8 +23,8 @@ async function routingContext(input) {
|
|
|
23
23
|
resetAt = Date.parse(event.meta.at);
|
|
24
24
|
}
|
|
25
25
|
const age = sentAt - Date.parse(event.meta.at);
|
|
26
|
-
const data = event.data;
|
|
27
|
-
if (event.type === "message.completed" && data !== void 0 && data.finishReason !== "tool-calls" && data.message.trim() !== "" && !isSilentReply(data.message) && age >= 0 && age <= 18e5) recovered.push(agentMessage({
|
|
26
|
+
const data = typeof event.data === "object" && event.data !== null ? event.data : void 0;
|
|
27
|
+
if (event.type === "message.completed" && data !== void 0 && typeof data.message === "string" && typeof data.finishReason === "string" && data.finishReason !== "tool-calls" && data.message.trim() !== "" && !isSilentReply(data.message) && age >= 0 && age <= 18e5) recovered.push(agentMessage({
|
|
28
28
|
text: data.message,
|
|
29
29
|
at: event.meta.at
|
|
30
30
|
}));
|
|
@@ -37,19 +37,16 @@ export interface SessionHistory {
|
|
|
37
37
|
/** Turns completed since `resetAt`; a compaction is worth nothing before the first. */
|
|
38
38
|
readonly turnsSinceReset: number;
|
|
39
39
|
}
|
|
40
|
-
/** One recorded event
|
|
40
|
+
/** One recorded event used by session policy, routing context and interaction projections. */
|
|
41
41
|
export interface RecordedEvent {
|
|
42
42
|
readonly type: string;
|
|
43
43
|
readonly meta: {
|
|
44
44
|
readonly at: string;
|
|
45
45
|
};
|
|
46
|
-
/**
|
|
47
|
-
readonly data?:
|
|
48
|
-
readonly message: string;
|
|
49
|
-
readonly finishReason: string;
|
|
50
|
-
};
|
|
46
|
+
/** Consumers validate the payload for the event type they project. */
|
|
47
|
+
readonly data?: unknown;
|
|
51
48
|
}
|
|
52
|
-
/**
|
|
49
|
+
/** Session events, oldest first; readers choose a tail or full stream for their consumers. */
|
|
53
50
|
export type HistoryReader = (sessionId: string) => Promise<ReadonlyArray<RecordedEvent>>;
|
|
54
51
|
/** How many events of the tail the reader fetches: enough for a day of a busy group. */
|
|
55
52
|
export declare const HISTORY_TAIL = 400;
|
|
@@ -4,9 +4,12 @@ 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 { actionApproval } from "../lib/confirmations.mjs";
|
|
8
|
+
import { actionToolName, ontologyToolLabels } from "../lib/ontology-tools.mjs";
|
|
7
9
|
import { attachmentForModel, attachmentOwner, optionalObject, optionalString, stringArgument, toolAttachments } from "../lib/attachment-tools.mjs";
|
|
8
10
|
import { defineTool } from "eve/tools";
|
|
9
11
|
var attachments_promote_default = defineTool({
|
|
12
|
+
approval: actionApproval({ labels: (memberId, _tool, input) => ontologyToolLabels()(memberId, actionToolName(stringArgument(input, "entity"), stringArgument(input, "action"))) }),
|
|
10
13
|
description: "Keep a file the member sent by storing it in a record through an ontology action that takes a file, such as document.upload. Give the entity, the action and the action's input without the file field: the platform fills it with the attachment, and fills sha256 when the action takes one. Runs with the member's permissions. The same file promoted again through the same action returns the earlier result instead of a duplicate.",
|
|
11
14
|
inputSchema: {
|
|
12
15
|
type: "object",
|