@segmentstream/agent 0.5.4 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +56 -18
  2. package/dist/extension/connections/ontology.mjs +2 -2
  3. package/dist/extension/instructions.md +10 -9
  4. package/dist/extension/lib/attachment-tools.mjs +1 -3
  5. package/dist/extension/lib/chat-post-confirmation.d.ts +23 -0
  6. package/dist/extension/lib/chat-post-confirmation.mjs +61 -0
  7. package/dist/extension/lib/confirmation-description.d.ts +2 -0
  8. package/dist/extension/lib/confirmations.d.ts +5 -0
  9. package/dist/extension/lib/confirmations.mjs +25 -5
  10. package/dist/extension/lib/conversations.d.ts +12 -1
  11. package/dist/extension/lib/conversations.mjs +4 -3
  12. package/dist/extension/lib/ontology-connection.d.ts +4 -0
  13. package/dist/extension/lib/ontology-connection.mjs +46 -3
  14. package/dist/extension/lib/ontology-tools.d.ts +1 -0
  15. package/dist/extension/lib/ontology-tools.mjs +27 -2
  16. package/dist/extension/lib/telegram/adapter.d.ts +1 -1
  17. package/dist/extension/lib/telegram/adapter.mjs +15 -26
  18. package/dist/extension/lib/telegram/chat-post-card.d.ts +15 -0
  19. package/dist/extension/lib/telegram/chat-post-card.mjs +65 -0
  20. package/dist/extension/lib/telegram/confirmations.d.ts +3 -2
  21. package/dist/extension/lib/telegram/confirmations.mjs +140 -21
  22. package/dist/extension/lib/telegram/customer-text.mjs +2 -1
  23. package/dist/extension/lib/telegram/health.mjs +1 -1
  24. package/dist/extension/lib/telegram/inbound-request.d.ts +1 -1
  25. package/dist/extension/lib/telegram/runtime-request.d.ts +8 -0
  26. package/dist/extension/lib/telegram/runtime-request.mjs +36 -0
  27. package/package.json +1 -1
  28. package/dist/extension/lib/telegram/requests.d.ts +0 -80
  29. package/dist/extension/lib/telegram/requests.mjs +0 -186
@@ -0,0 +1,65 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ const MAX_PREVIEW_LENGTH = 3584;
9
+ const exactText = new AsyncLocalStorage();
10
+ function withChatPostCard(text, run) {
11
+ if (text.length > 4096) throw new Error("The complete chat confirmation cannot fit in one Telegram message.");
12
+ return exactText.run(text, run);
13
+ }
14
+ function isExactChatPostCard(text) {
15
+ return exactText.getStore() === text;
16
+ }
17
+ function chatPostCopy(locale) {
18
+ return locale.startsWith("ru") ? {
19
+ edit: "Изменить",
20
+ changes: "Что изменить в сообщении?",
21
+ uncertain: "Неизвестно, отправлено ли сообщение. Не отправляйте его повторно автоматически.",
22
+ sent: "Отправлено.",
23
+ history: "Не удалось записать квитанцию в историю.",
24
+ expired: "Срок подтверждения истёк. Ничего не отправлено."
25
+ } : {
26
+ edit: "Edit",
27
+ changes: "What would you like to change in the message?",
28
+ uncertain: "Delivery is uncertain. Do not resend automatically.",
29
+ sent: "Sent.",
30
+ history: "The receipt could not be recorded in history.",
31
+ expired: "This confirmation expired. Nothing was sent."
32
+ };
33
+ }
34
+ function chatPostCard(preview, locale) {
35
+ const russian = locale.startsWith("ru");
36
+ const text = [
37
+ russian ? "Отправить сообщение?" : "Send message?",
38
+ `${russian ? "Чат" : "Chat"}: ${preview.title} (${preview.provider})`,
39
+ ...preview.replyTo === null ? [] : [`${russian ? "В ответ на" : "Reply to"}: ${preview.replyTo}`],
40
+ "",
41
+ preview.finalText
42
+ ].join("\n");
43
+ if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
44
+ return {
45
+ text,
46
+ confirm: russian ? "Отправить" : "Confirm",
47
+ cancel: russian ? "Отмена" : "Cancel",
48
+ executing: russian ? "Подтверждено." : "Confirmed.",
49
+ cancelled: russian ? "Отменено." : "Cancelled.",
50
+ completed: chatPostCopy(locale).sent,
51
+ failed: russian ? "Ошибка отправки сообщения." : "Chat posting failed."
52
+ };
53
+ }
54
+ function chatPostReceipt(output, locale) {
55
+ const value = typeof output === "object" && output !== null ? output : {};
56
+ if (value.status !== "sent" && value.status !== "uncertain" || typeof value.history_recorded !== "boolean" || typeof value.duplicate !== "boolean" || value.message_id !== null && typeof value.message_id !== "string" || value.url !== null && typeof value.url !== "string") return;
57
+ const copy = chatPostCopy(locale);
58
+ const url = typeof value.url === "string" && value.url.length <= 256 && /^https:\/\/t\.me\/[A-Za-z0-9_/-]+$/u.test(value.url) ? value.url : null;
59
+ return [
60
+ value.status === "sent" ? copy.sent : copy.uncertain,
61
+ ...url === null ? [] : [url],
62
+ ...value.history_recorded ? [] : [copy.history]
63
+ ].join("\n");
64
+ }
65
+ export { chatPostCard, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
@@ -1,10 +1,11 @@
1
1
  import type { ChannelEvents } from "eve/channels";
2
2
  import type { TelegramCallbackQuery, TelegramChannelEvents, TelegramContext, TelegramEventContext } from "eve/channels/telegram";
3
+ import { type Conversations } from "../conversations.js";
3
4
  import type { AgentIdentities } from "../identities.js";
4
5
  import { type PendingQuestion } from "../questions.js";
5
6
  import { type ConfirmationRenderer } from "./confirmation-text.js";
6
7
  import { type HistoryReader, type RecordedEvent } from "./sessions.js";
7
- export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel"): string;
8
+ export declare function confirmationCallback(requestId: string, memberId: string, decision: "approve" | "cancel" | "edit"): string;
8
9
  export declare function questionFromHistory(events: readonly RecordedEvent[], owner?: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): PendingQuestion | undefined;
9
10
  export declare function questionAddress(owner: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
10
11
  /** Custom callback data avoids Eve's default, unpaired Telegram callback principal. */
@@ -16,5 +17,5 @@ export declare function handleConfirmationCallback(input: {
16
17
  readonly scope: "chat" | "thread";
17
18
  readonly now?: number;
18
19
  }): Promise<void>;
19
- export declare function telegramConfirmationEvents(render?: ConfirmationRenderer): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
+ export declare function telegramConfirmationEvents(render?: ConfirmationRenderer, archive?: Conversations): TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
20
21
  export declare const confirmationEvents: TelegramChannelEvents & Pick<ChannelEvents<TelegramEventContext>, "context.cleared">;
@@ -5,7 +5,10 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversationLanguage, memberIdOf, telegramPrincipal } from "./principal.mjs";
8
+ import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
9
+ import { chatPostCard, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
8
10
  import { confirmations } from "../confirmations.mjs";
11
+ import { conversationEvent } from "../conversations.mjs";
9
12
  import { currentInboundRequest } from "./inbound-request.mjs";
10
13
  import { currentLocale, failureText, reportTelegramError } from "./customer-text.mjs";
11
14
  import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
@@ -18,7 +21,8 @@ function shortHash(value) {
18
21
  return createHash("sha256").update(value).digest("hex").slice(0, 20);
19
22
  }
20
23
  function confirmationCallback(requestId, memberId, decision) {
21
- return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${decision === "approve" ? "y" : "n"}`;
24
+ const choice = decision === "approve" ? "y" : decision === "edit" ? "e" : "n";
25
+ return `${PREFIX}${shortHash(requestId)}:${shortHash(memberId)}:${choice}`;
22
26
  }
23
27
  function object(value) {
24
28
  return typeof value === "object" && value !== null ? value : {};
@@ -34,7 +38,9 @@ function openConfirmations(events) {
34
38
  if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string") open.set(request.requestId, {
35
39
  requestId: request.requestId,
36
40
  callId: action.callId,
37
- at: Date.parse(event.meta.at)
41
+ at: Date.parse(event.meta.at),
42
+ toolName: String(action.toolName ?? ""),
43
+ input: object(action.input)
38
44
  });
39
45
  }
40
46
  if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const item of data.resolutions) {
@@ -49,6 +55,10 @@ function questionFromHistory(events, owner) {
49
55
  for (const event of events) {
50
56
  const data = object(event.data);
51
57
  if (event.type === "context.cleared") pending.clear();
58
+ if (event.type === "input.resolved" && Array.isArray(data.resolutions)) for (const resolution of data.resolutions) {
59
+ const question = chatPostEditQuestion(object(resolution));
60
+ if (question !== void 0) pending.set(questionKey(question), question);
61
+ }
52
62
  if (event.type === "action.result") {
53
63
  const result = object(data.result);
54
64
  const candidate = object(result.output).pendingQuestion;
@@ -60,6 +70,16 @@ function questionFromHistory(events, owner) {
60
70
  }
61
71
  return owner === void 0 ? [...pending.values()].at(-1) : pending.get(questionKey(owner));
62
72
  }
73
+ function chatPostEditQuestion(resolution) {
74
+ const text = object(resolution.response).text;
75
+ if (resolution.outcome !== "cancelled" || typeof text !== "string") return;
76
+ try {
77
+ const question = object(JSON.parse(text)).pendingChatPostEdit;
78
+ return isPendingQuestion(question) ? question : void 0;
79
+ } catch {
80
+ return;
81
+ }
82
+ }
63
83
  function questionAddress(owner) {
64
84
  return formatAddress({
65
85
  ...owner,
@@ -111,12 +131,17 @@ async function handleConfirmationCallback(input) {
111
131
  }), input.scope);
112
132
  const session = await (currentInboundRequest()?.sessions)?.resolveSession(address);
113
133
  const prompt = session === void 0 ? void 0 : openConfirmations(await input.read(session.id)).find((candidate) => shortHash(candidate.requestId) === requestHash);
114
- if (prompt === void 0 || choice !== "y" && choice !== "n") {
134
+ if (prompt === void 0 || ![
135
+ "y",
136
+ "n",
137
+ "e"
138
+ ].includes(choice ?? "") || choice === "e" && !isChatPost(prompt.toolName)) {
115
139
  await notice("This confirmation is no longer pending.");
116
140
  return;
117
141
  }
118
142
  const expired = !Number.isFinite(prompt.at) || prompt.at + 6e5 <= (input.now ?? Date.now());
119
- const decision = expired || choice === "n" ? "cancel" : "approve";
143
+ const decision = expired || choice !== "y" ? "cancel" : "approve";
144
+ const edit = !expired && choice === "e";
120
145
  const principal = telegramPrincipal({
121
146
  member: lookup.member,
122
147
  message: {
@@ -130,17 +155,58 @@ async function handleConfirmationCallback(input) {
130
155
  });
131
156
  const source = currentInboundRequest()?.respond;
132
157
  if (source === void 0) throw new Error("Confirmation callback has no session continuation.");
158
+ const editQuestion = edit ? {
159
+ id: `edit-${prompt.callId}`,
160
+ memberId: lookup.member.id,
161
+ chatId: query.message.chat.id,
162
+ messageThreadId: String(query.message.messageThreadId ?? ""),
163
+ address,
164
+ question: chatPostCopy(language).changes,
165
+ task: `The previous chat post was cancelled for editing. Apply the member's changes to this draft, then make a NEW chat_post_message call and confirmation. Never reuse the cancelled call. Previous input: ${JSON.stringify(prompt.input)}`
166
+ } : void 0;
133
167
  await source(address, prompt.requestId, decision, {
134
168
  ...principal,
135
169
  attributes: {
136
170
  ...principal.attributes,
137
171
  confirmationRequestId: prompt.requestId,
138
- confirmationDecision: decision
172
+ confirmationDecision: decision,
173
+ ...expired ? { confirmationReason: "expired" } : edit ? { confirmationReason: "edit" } : {}
139
174
  }
140
- });
175
+ }, editQuestion === void 0 ? void 0 : JSON.stringify({ pendingChatPostEdit: editQuestion }));
141
176
  await notice(expired ? "This confirmation expired; nothing executed." : decision === "cancel" ? "Cancellation received." : "Confirmation received.");
142
177
  }
143
- function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale)) {
178
+ function telegramConfirmationEvents(render = async (description, locale) => fallbackConfirmation(description, locale), archive) {
179
+ const record = async (callId, phase, text, status, ctx) => {
180
+ const saved = confirmations.get()[callId];
181
+ if (archive === void 0 || saved?.description?.chatPost === void 0 || saved.source === void 0) return;
182
+ const operationId = chatPostOperationId(ctx.session.id, callId);
183
+ const source = saved.source;
184
+ const append = () => archive.append(conversationEvent({
185
+ eventId: `chat-post:${operationId}:${phase}`,
186
+ provider: "telegram",
187
+ chatId: source.chatId,
188
+ chatType: source.chatId.startsWith("-") ? "group" : "private",
189
+ threadId: source.threadId || null,
190
+ participantIds: [saved.memberId],
191
+ kind: "tool_result",
192
+ operationId,
193
+ toolCallId: operationId,
194
+ toolName: saved.action,
195
+ sessionId: ctx.session.id,
196
+ text,
197
+ status,
198
+ reason: phase
199
+ }));
200
+ if (phase === "preview") await append();
201
+ else try {
202
+ await append();
203
+ } catch (error) {
204
+ reportTelegramError("chat_post_source_archive_failed", error, {
205
+ operationId,
206
+ phase
207
+ });
208
+ }
209
+ };
144
210
  return {
145
211
  "context.cleared"(_data, channel) {
146
212
  confirmations.update(() => ({}));
@@ -176,18 +242,30 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
176
242
  }
177
243
  const memberId = confirmations.get()[request.action.callId]?.memberId ?? memberIdOf(ctx.session.auth.current);
178
244
  if (memberId === void 0) throw new Error("Confirmation requires a member.");
179
- const copy = await render(confirmations.get()[request.action.callId]?.description, confirmationLocale(ctx.session.auth.current));
245
+ const description = confirmations.get()[request.action.callId]?.description;
246
+ const exactPost = isChatPost(request.action.toolName);
247
+ if (exactPost && description?.chatPost === void 0) throw new Error("An exact chat post cannot show a generic confirmation.");
248
+ const copy = description?.chatPost === void 0 ? await render(description, confirmationLocale(ctx.session.auth.current)) : chatPostCard(description.chatPost, confirmationLocale(ctx.session.auth.current));
180
249
  const text = copy.text;
181
- const sent = await channel.telegram.post({
250
+ await record(request.action.callId, "preview", text, "requested", ctx);
251
+ const send = () => channel.telegram.post({
182
252
  text,
183
- reply_markup: { inline_keyboard: [[{
184
- text: copy.confirm,
185
- callback_data: confirmationCallback(request.requestId, memberId, "approve")
186
- }, {
187
- text: copy.cancel,
188
- callback_data: confirmationCallback(request.requestId, memberId, "cancel")
189
- }]] }
253
+ reply_markup: { inline_keyboard: [[
254
+ {
255
+ text: copy.confirm,
256
+ callback_data: confirmationCallback(request.requestId, memberId, "approve")
257
+ },
258
+ ...exactPost ? [{
259
+ text: chatPostCopy(confirmationLocale(ctx.session.auth.current)).edit,
260
+ callback_data: confirmationCallback(request.requestId, memberId, "edit")
261
+ }] : [],
262
+ {
263
+ text: copy.cancel,
264
+ callback_data: confirmationCallback(request.requestId, memberId, "cancel")
265
+ }
266
+ ]] }
190
267
  });
268
+ const sent = await (exactPost ? withChatPostCard(text, send) : send());
191
269
  const state = channel.state;
192
270
  state.confirmationMessages = {
193
271
  ...state.confirmationMessages,
@@ -196,7 +274,8 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
196
274
  messageId: sent.id,
197
275
  text,
198
276
  copy,
199
- locale: confirmationLocale(ctx.session.auth.current)
277
+ locale: confirmationLocale(ctx.session.auth.current),
278
+ exactPost
200
279
  }
201
280
  };
202
281
  }
@@ -221,12 +300,39 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
221
300
  }
222
301
  const [callId, message] = entry;
223
302
  const copy = message.copy ?? confirmationCopy(message.locale ?? confirmationLocale(ctx.session.auth.current, currentLocale()));
224
- const text = resolution.outcome === "approved" ? copy.executing : copy.cancelled;
225
- await channel.telegram.editMessageText({
303
+ const reason = confirmations.get()[callId]?.cancellationReason;
304
+ const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? chatPostCopy(message.locale ?? "en").expired : copy.cancelled;
305
+ const text = message.exactPost ? `${message.text}\n\n${status}` : status;
306
+ const edit = () => channel.telegram.editMessageText({
226
307
  messageId: message.messageId,
227
308
  replyMarkup: { inline_keyboard: [] },
228
309
  text
229
310
  });
311
+ await (message.exactPost ? withChatPostCard(text, edit) : edit());
312
+ await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "completed" : "cancelled", ctx);
313
+ const question = chatPostEditQuestion(resolution);
314
+ if (message.exactPost && reason === "edit" && question !== void 0) {
315
+ pendingQuestions.update((all) => ({
316
+ ...all,
317
+ [questionKey(question)]: question
318
+ }));
319
+ state.pendingQuestions = {
320
+ ...state.pendingQuestions,
321
+ [questionKey(question)]: question
322
+ };
323
+ channel.continuation?.alias(questionAddress(question));
324
+ channel.continuation?.alias(formatAddress({
325
+ ...question,
326
+ conversationId: ""
327
+ }));
328
+ await channel.telegram.post({
329
+ text: question.question,
330
+ reply_markup: {
331
+ force_reply: true,
332
+ selective: true
333
+ }
334
+ });
335
+ }
230
336
  if (resolution.outcome !== "approved") {
231
337
  delete state.confirmationMessages?.[callId];
232
338
  confirmations.update((all) => ({
@@ -239,7 +345,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
239
345
  }
240
346
  }
241
347
  },
242
- async "action.result"(data, channel) {
348
+ async "action.result"(data, channel, ctx) {
243
349
  const state = channel.state;
244
350
  if (data.result.kind === "tool-result") {
245
351
  const question = object(data.result.output).pendingQuestion;
@@ -278,7 +384,20 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
278
384
  }
279
385
  const message = state.confirmationMessages?.[data.result.callId];
280
386
  if (message === void 0) return;
281
- if (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)) {
387
+ 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);
388
+ if (message.exactPost) {
389
+ const locale = message.locale ?? currentLocale();
390
+ const receipt = !failed && data.result.kind === "tool-result" ? chatPostReceipt(data.result.output, locale) : void 0;
391
+ const errorId = failed ? reportTelegramError("confirmed_chat_post_failed", data, { callId: data.result.callId }) : void 0;
392
+ const status = receipt ?? (failed ? `${message.copy?.failed ?? "Chat posting failed."} ${errorId}\n${chatPostCopy(locale).uncertain}` : chatPostCopy(locale).uncertain);
393
+ const text = `${message.text}\n\n${status}`;
394
+ await withChatPostCard(text, () => channel.telegram.editMessageText({
395
+ messageId: message.messageId,
396
+ replyMarkup: { inline_keyboard: [] },
397
+ text
398
+ }));
399
+ await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
400
+ } else if (failed) {
282
401
  const errorId = reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId });
283
402
  await channel.telegram.editMessageText({
284
403
  messageId: message.messageId,
@@ -4,6 +4,7 @@ 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 { isExactChatPostCard } from "./chat-post-card.mjs";
7
8
  import { redactDiagnostic } from "../diagnostics.mjs";
8
9
  import { currentInboundRequest } from "./inbound-request.mjs";
9
10
  import { randomBytes } from "node:crypto";
@@ -97,7 +98,7 @@ function safeTelegramFetch(fetchImpl) {
97
98
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
98
99
  if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
99
100
  const body = JSON.parse(init.body);
100
- for (const field of ["text", "caption"]) if (typeof body[field] === "string") body[field] = customerText(body[field]);
101
+ for (const field of ["text", "caption"]) if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) body[field] = customerText(body[field]);
101
102
  if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
102
103
  ...item,
103
104
  ...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
@@ -4,7 +4,7 @@ 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 { runtimeRequestFailure } from "./requests.mjs";
7
+ import { runtimeRequestFailure } from "./runtime-request.mjs";
8
8
  import { GET } from "eve/channels";
9
9
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
10
10
  function healthRoute(input) {
@@ -42,7 +42,7 @@ export declare class InboundRequest {
42
42
  waitUntil: ((task: Promise<unknown>) => void) | undefined;
43
43
  /** Continues the exact pending question's session, including under thread scope. */
44
44
  address: string | undefined;
45
- respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext) => Promise<unknown>) | undefined;
45
+ respond: ((address: string, requestId: string, decision: "approve" | "cancel", principal: SessionAuthContext, text?: string) => Promise<unknown>) | undefined;
46
46
  constructor(
47
47
  /** The agent's description from the compiled manifest, for the routing decision. */
48
48
  agentDescription: () => Promise<string>);
@@ -0,0 +1,8 @@
1
+ import type { RuntimeTokenVerifier } from "../runtime-identity.js";
2
+ /** Authenticate internal channel routes with the production deployment's runtime identity. */
3
+ export declare function runtimeRequestFailure(request: Request, options: {
4
+ readonly enabled: () => boolean;
5
+ readonly configured: () => boolean;
6
+ readonly verify: () => RuntimeTokenVerifier;
7
+ readonly log?: (message: string, detail: unknown) => void;
8
+ }): Promise<Response | undefined>;
@@ -0,0 +1,36 @@
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
+ function bearer(header) {
8
+ return (header === null ? null : /^Bearer\s+(\S+)$/i.exec(header.trim()))?.[1];
9
+ }
10
+ function error(code, status) {
11
+ return Response.json({ error: code }, { status });
12
+ }
13
+ async function runtimeRequestFailure(request, options) {
14
+ const log = options.log ?? ((message, detail) => console.warn(message, detail));
15
+ if (!options.enabled()) return error("channels_disabled", 503);
16
+ if (!options.configured()) return error("not_found", 404);
17
+ const token = bearer(request.headers.get("authorization"));
18
+ if (token === void 0) return error("unauthorized", 401);
19
+ let verifier;
20
+ try {
21
+ verifier = options.verify();
22
+ } catch (cause) {
23
+ log("The agent cannot verify requests: the runtime identity is not configured.", cause);
24
+ return error("unavailable", 503);
25
+ }
26
+ const verification = await verifier(token);
27
+ if (verification.status === "unavailable") {
28
+ log("The agent cannot verify requests: the runtime key set is unavailable.", verification.reason);
29
+ return error("unavailable", 503);
30
+ }
31
+ if (verification.status !== "valid") {
32
+ log("The agent refused a request with an invalid runtime token.", verification.reason);
33
+ return error("unauthorized", 401);
34
+ }
35
+ }
36
+ export { runtimeRequestFailure };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.5.4",
3
+ "version": "0.6.3",
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,80 +0,0 @@
1
- import { type RouteDefinition } from "eve/channels";
2
- import type { TelegramChannel, TelegramChannelState } from "eve/channels/telegram";
3
- import type { SessionAuthContext } from "eve/context";
4
- import type { SessionSettings } from "../../config.js";
5
- import type { RuntimeTokenVerifier } from "../runtime-identity.js";
6
- import { type SessionPolicy } from "./sessions.js";
7
- /**
8
- * Requests from the ontology's `agent_chat.send_message` action: an external agent or a member asks
9
- * the workspace agent to contact someone in a group or a private chat. The ontology service resolves
10
- * the chat and posts here with its own Vercel OIDC token; the request is delivered into the chat's
11
- * session through eve's proactive `receive`, without a seed message, so it continues the session
12
- * the chat's own messages reach (`chatId::`, the chat-scope address) and the replies land there too.
13
- * The session policy runs on that session first, as it does for an inbound message.
14
- */
15
- export declare const MESSAGES_ROUTE = "/eve/v1/telegram/messages";
16
- export interface AgentRequest {
17
- readonly target: {
18
- readonly kind: "group" | "private";
19
- readonly provider: "telegram";
20
- /** The group's chat id, or the Telegram user id of a private chat. */
21
- readonly chatId: string;
22
- };
23
- readonly message: string;
24
- /** Document record ids from newer ontologies; omitted by older deployments. */
25
- readonly documentIds?: ReadonlyArray<string>;
26
- /** Whom to contact, when the requester named someone. */
27
- readonly person: {
28
- readonly id: string;
29
- readonly name: string;
30
- } | null;
31
- readonly requester: {
32
- readonly id: string;
33
- readonly name: string;
34
- readonly kind: "member" | "service";
35
- /** The roles the requester holds; empty from an ontology that does not send them. */
36
- readonly roles: ReadonlyArray<string>;
37
- /**
38
- * Whether the turn can act as the requester: a member with a live paired identity. An ontology
39
- * that does not send it means a member can.
40
- */
41
- readonly actAs: boolean;
42
- };
43
- }
44
- /** The request body, or `undefined` when it does not match the contract. */
45
- export declare function agentRequest(value: unknown): AgentRequest | undefined;
46
- /**
47
- * Whom the request's turn runs as. A member requester the ontology says can be acted as is the
48
- * turn's member, so the ontology connection acts as them and the ontology service checks, as for any
49
- * turn, that they are active and paired. Their roles, as the action's principal held them, choose the
50
- * model of a session the request starts; the service resolves them afresh on every call. A service
51
- * user, or a member who has not paired, cannot be acted as, so the request runs without a member and
52
- * the turn has no ontology; their roles still choose the model, and grant nothing, since the
53
- * ontology connection acts only for a `memberId`.
54
- */
55
- export declare function requestPrincipal(request: AgentRequest): SessionAuthContext;
56
- /** What the turn is about, for the model, ahead of the request itself. */
57
- export declare function requestContext(request: AgentRequest): string;
58
- /** Authenticate internal channel routes with the production deployment's runtime identity. */
59
- export declare function runtimeRequestFailure(request: Request, options: {
60
- readonly enabled: () => boolean;
61
- readonly configured: () => boolean;
62
- readonly verify: () => RuntimeTokenVerifier;
63
- readonly log?: (message: string, detail: unknown) => void;
64
- }): Promise<Response | undefined>;
65
- /**
66
- * `POST /eve/v1/telegram/messages`. Gated like the webhook: outside production it answers 503 and a
67
- * workspace without a Telegram channel 404. The caller must be a function of this deployment's
68
- * project, verified from its Vercel OIDC token.
69
- */
70
- export declare function messagesRoute(input: {
71
- readonly channel: TelegramChannel;
72
- readonly enabled: () => boolean;
73
- readonly configured: () => boolean;
74
- readonly verify: () => RuntimeTokenVerifier;
75
- /** The adapter's session policy, which the webhook route applies too. */
76
- readonly sessionPolicy: SessionPolicy;
77
- /** The mounted `sessions.scope`. */
78
- readonly scope: () => SessionSettings["scope"];
79
- readonly log?: (message: string, detail: unknown) => void;
80
- }): RouteDefinition<TelegramChannelState>;