@segmentstream/agent 0.3.0 → 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.
Files changed (29) hide show
  1. package/dist/extension/_manifest.json +2 -1
  2. package/dist/extension/instructions.md +21 -6
  3. package/dist/extension/lib/confirmations.d.ts +26 -0
  4. package/dist/extension/lib/confirmations.mjs +96 -0
  5. package/dist/extension/lib/ontology-connection.d.ts +1 -0
  6. package/dist/extension/lib/ontology-connection.mjs +3 -0
  7. package/dist/extension/lib/ontology-tools.d.ts +11 -0
  8. package/dist/extension/lib/ontology-tools.mjs +41 -0
  9. package/dist/extension/lib/questions.d.ts +14 -0
  10. package/dist/extension/lib/questions.mjs +30 -0
  11. package/dist/extension/lib/telegram/adapter.d.ts +1 -1
  12. package/dist/extension/lib/telegram/adapter.mjs +31 -4
  13. package/dist/extension/lib/telegram/confirmations.d.ts +18 -0
  14. package/dist/extension/lib/telegram/confirmations.mjs +280 -0
  15. package/dist/extension/lib/telegram/inbound-request.d.ts +4 -0
  16. package/dist/extension/lib/telegram/inbound-request.mjs +2 -0
  17. package/dist/extension/lib/telegram/inbound.d.ts +3 -0
  18. package/dist/extension/lib/telegram/inbound.mjs +50 -9
  19. package/dist/extension/lib/telegram/principal.mjs +1 -0
  20. package/dist/extension/lib/telegram/requests.mjs +3 -1
  21. package/dist/extension/lib/telegram/router.d.ts +1 -1
  22. package/dist/extension/lib/telegram/routing-context.mjs +2 -2
  23. package/dist/extension/lib/telegram/sessions.d.ts +4 -7
  24. package/dist/extension/tools/attachments_promote.mjs +3 -0
  25. package/dist/extension/tools/questions_ask.d.ts +12 -0
  26. package/dist/extension/tools/questions_ask.mjs +50 -0
  27. package/dist/tools/index.d.ts +1 -0
  28. package/dist/tools/index.mjs +2 -1
  29. package/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  "channel": 58,
9
9
  "connection": 46,
10
10
  "instructions": 2,
11
- "config": 1
11
+ "config": 1,
12
+ "state": 13
12
13
  }
13
14
  }
@@ -7,15 +7,30 @@ talking to you, and you act with exactly their permissions.
7
7
  connection. Call `describe` for the catalog of entities and actions, and `me` for whom you act
8
8
  for and what they may do. The tools you see are already narrowed to that member's permissions;
9
9
  if a tool is missing, the member may not use it, so say so instead of looking for a way around.
10
- - Every change goes through an action. Never claim a record changed unless an action reported it.
10
+ - When the current member has permission and gives a clear instruction, act, including necessary
11
+ routine intermediate steps. Their authorization continues across turns. When they answer your
12
+ question, use the answer and continue the pending task without asking for permission again.
13
+ - Ask only when information is missing or ambiguous. In Telegram, use
14
+ `segmentstream__questions_ask` with the question and pending task so the session keeps its intent,
15
+ member and chat/topic. An answer to a pending question is never chatter or `[SILENT]`.
16
+ - Destructive or irreversible actions, and legacy custom actions without safety labels, require
17
+ the tool boundary's Confirm / Cancel buttons. Call the action to start that flow; never ask for
18
+ approval in prose or treat a text acknowledgement as approval. Wait for the button outcome.
19
+ Cancel ends that call; retry it only after a new instruction from the member.
20
+ - Every change goes through an action. Report results after success; never claim a record changed
21
+ unless an action reported it.
11
22
  - Never invent records or values. Say when the ontology does not hold what was asked.
12
23
  - When you have been routed a message but a reply would add nothing, answer with exactly
13
24
  `[SILENT]` and nothing else. Use it only in that case; it suppresses delivery to the chat.
14
- - To ask someone something in a group or a private chat, run the ontology action
15
- `agent_chat.send_message`; their replies come back into that chat's session, where you handle
16
- them. The groups you are in are listed in `agent_chat`, with the `purpose` and `whenToUse` members
17
- set; when a member who may change it tells you what a group is for, record it with
18
- `agent_chat.update`.
25
+ - Use the ontology action `agent_chat.send_message` only to start a conversation in a different
26
+ group or private chat; their replies come back into that chat's session, where you handle them.
27
+ Inside a delivered `<agent_request>`, this turn already runs in the target chat: your reply here
28
+ is the message delivered to that chat. Do not look for or call `agent_chat.send_message` to deliver
29
+ it, even if the ontology catalog is empty or that action is unavailable. If the addressed person
30
+ has a linked Telegram username, mention them with @username; otherwise address them by name
31
+ without a tag. Never invent a username. The groups you are in are listed in `agent_chat`, with the
32
+ `purpose` and `whenToUse` members set; when a member who may change it tells you what a group is
33
+ for, record it with `agent_chat.update`.
19
34
  - Files the member sends are temporary attachments, listed in an `<attachments>` block with their
20
35
  ids and kept for 24 hours. `segmentstream__attachments_read` extracts sheet tables, document text,
21
36
  email bodies and archive listings, with images/PDF inline and metadata for other formats. Follow
@@ -0,0 +1,26 @@
1
+ import { type StateHandle } from "eve/context";
2
+ import type { ApprovalConfiguration } from "eve/tools/approval";
3
+ export declare const CONFIRMATION_TTL_MS: number;
4
+ export interface ActionAnnotations {
5
+ readonly readOnlyHint?: boolean;
6
+ readonly destructiveHint?: boolean;
7
+ readonly irreversibleHint?: boolean;
8
+ readonly idempotentHint?: boolean;
9
+ }
10
+ /** Safety comes from the authenticated ontology, never from the name or model prose. */
11
+ export declare function needsConfirmation(labels: ActionAnnotations): boolean;
12
+ export declare function inputHash(input: unknown): string;
13
+ export interface Confirmation {
14
+ readonly memberId: string;
15
+ readonly action: string;
16
+ readonly inputHash: string;
17
+ readonly expiresAt: number;
18
+ readonly status: "pending" | "approved" | "consumed" | "cancelled";
19
+ }
20
+ export declare const confirmations: StateHandle<Record<string, Confirmation>>;
21
+ /** Eve pauses the original call and rechecks this policy immediately before executing it. */
22
+ export declare function actionApproval(options: {
23
+ readonly labels: (memberId: string, action: string, input: Readonly<Record<string, unknown>>) => Promise<ActionAnnotations>;
24
+ readonly state?: StateHandle<Record<string, Confirmation>>;
25
+ readonly now?: () => number;
26
+ }): ApprovalConfiguration;
@@ -0,0 +1,96 @@
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 } from "./telegram/principal.mjs";
8
+ import { createHash } from "node:crypto";
9
+ import { defineState } from "eve/context";
10
+ const CONFIRMATION_TTL_MS = 6e5;
11
+ function needsConfirmation(labels) {
12
+ return labels.readOnlyHint !== true && (labels.destructiveHint === true || labels.irreversibleHint === true || labels.destructiveHint === void 0 && labels.irreversibleHint === void 0);
13
+ }
14
+ function canonical(value) {
15
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
16
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
17
+ const encoded = JSON.stringify(value);
18
+ if (encoded === void 0) throw new TypeError("Action input must be JSON.");
19
+ return encoded;
20
+ }
21
+ function inputHash(input) {
22
+ return createHash("sha256").update(canonical(input)).digest("hex");
23
+ }
24
+ const confirmations = defineState("segmentstream.confirmations", () => ({}));
25
+ function actionApproval(options) {
26
+ const state = options.state ?? confirmations;
27
+ const now = options.now ?? Date.now;
28
+ const denied = (reason) => ({
29
+ type: "denied",
30
+ reason
31
+ });
32
+ return {
33
+ async request(ctx) {
34
+ const memberId = memberIdOf(ctx.session.auth.current);
35
+ if (memberId === void 0) return denied("A paired member is required.");
36
+ const hash = inputHash(ctx.toolInput ?? {});
37
+ const saved = state.get()[ctx.callId];
38
+ if (saved !== void 0) {
39
+ if (saved.memberId !== memberId || saved.action !== ctx.toolName || saved.inputHash !== hash) return denied("The confirmation belongs to another member, action or input.");
40
+ if (saved.expiresAt <= now()) return denied("The confirmation expired. Make a new call.");
41
+ if (saved.status === "consumed" || saved.status === "cancelled") return denied("The confirmation has already been used or cancelled.");
42
+ }
43
+ let labels;
44
+ try {
45
+ labels = await options.labels(memberId, ctx.toolName, ctx.toolInput ?? {});
46
+ } catch (error) {
47
+ console.warn("The ontology action permission check failed.", error);
48
+ return denied("The action is unavailable or the member no longer has permission.");
49
+ }
50
+ if (saved?.status === "approved") {
51
+ state.update((all) => ({
52
+ ...all,
53
+ [ctx.callId]: {
54
+ ...saved,
55
+ status: "consumed"
56
+ }
57
+ }));
58
+ return "approved";
59
+ }
60
+ if (saved !== void 0) return denied("The original call is waiting for its Confirm / Cancel buttons.");
61
+ if (!needsConfirmation(labels)) return "approved";
62
+ state.update((all) => ({
63
+ ...all,
64
+ [ctx.callId]: {
65
+ memberId,
66
+ action: ctx.toolName,
67
+ inputHash: hash,
68
+ expiresAt: now() + CONFIRMATION_TTL_MS,
69
+ status: "pending"
70
+ }
71
+ }));
72
+ return "user-approval";
73
+ },
74
+ response(ctx) {
75
+ const saved = state.get()[ctx.request.callId];
76
+ const reject = (reason) => ({
77
+ status: "rejected",
78
+ reason
79
+ });
80
+ if (saved === void 0 || saved.status !== "pending") return reject("This confirmation is no longer pending.");
81
+ if (ctx.response.principal.attributes.confirmationRequestId !== ctx.request.requestId || ctx.response.principal.attributes.confirmationDecision !== ctx.response.decision) return reject("Use the Confirm or Cancel button on the action's message.");
82
+ if (memberIdOf(ctx.response.principal) !== saved.memberId || memberIdOf(ctx.request.principal) !== saved.memberId) return reject("Only the requesting member can confirm or cancel this action.");
83
+ if (saved.action !== ctx.request.toolName || saved.inputHash !== inputHash(ctx.request.toolInput ?? {})) return reject("The action or input changed. Make a new call.");
84
+ if (ctx.response.decision === "approve" && saved.expiresAt <= now()) return reject("This confirmation expired.");
85
+ state.update((all) => ({
86
+ ...all,
87
+ [ctx.request.callId]: {
88
+ ...saved,
89
+ status: ctx.response.decision === "approve" ? "approved" : "cancelled"
90
+ }
91
+ }));
92
+ return { status: "allowed" };
93
+ }
94
+ };
95
+ }
96
+ export { CONFIRMATION_TTL_MS, actionApproval, confirmations, inputHash, needsConfirmation };
@@ -33,4 +33,5 @@ export declare function ontologyConnection(input: {
33
33
  readonly principal: SessionAuthContext | null;
34
34
  readonly environment?: Environment;
35
35
  readonly token?: RuntimeToken;
36
+ readonly fetch?: typeof fetch;
36
37
  }): McpClientConnectionDefinition | null;
@@ -6,7 +6,9 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { appOrigin } from "./environment.mjs";
8
8
  import { memberIdOf } from "./telegram/principal.mjs";
9
+ import { actionApproval } from "./confirmations.mjs";
9
10
  import { runtimeToken, tokenExpiry } from "./runtime-token.mjs";
11
+ import { ontologyToolLabels } from "./ontology-tools.mjs";
10
12
  import { defineMcpClientConnection } from "eve/connections";
11
13
  const ACT_AS_HEADER = "X-SegmentStream-Act-As";
12
14
  const DESCRIPTION = "The company's ontology: its records, their history, and the actions that change them, narrowed to what the member you act for may read and do. Call describe for the catalog and me for the member.";
@@ -28,6 +30,7 @@ function ontologyConnection(input) {
28
30
  url: `${origin}/mcp`,
29
31
  description: DESCRIPTION,
30
32
  instanceKey: `${origin}/mcp#member:${memberId}`,
33
+ approval: actionApproval({ labels: ontologyToolLabels(input) }),
31
34
  auth: {
32
35
  credentialOwner: "app",
33
36
  getToken: async () => {
@@ -0,0 +1,11 @@
1
+ import type { ActionAnnotations } from "./confirmations.js";
2
+ import { type Environment } from "./environment.js";
3
+ import { type RuntimeToken } from "./runtime-token.js";
4
+ /** A fresh, permission-filtered catalog. Listing tools cannot run an action's prepare step. */
5
+ export declare function ontologyToolLabels(options?: {
6
+ readonly environment?: Environment;
7
+ readonly token?: RuntimeToken;
8
+ readonly fetch?: typeof fetch;
9
+ }): (memberId: string, action: string) => Promise<ActionAnnotations>;
10
+ /** MCP normalizes an authored camelCase action to snake_case. */
11
+ export declare function actionToolName(entity: string, action: string): string;
@@ -0,0 +1,41 @@
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 { appOrigin } from "./environment.mjs";
8
+ import { runtimeToken } from "./runtime-token.mjs";
9
+ function ontologyToolLabels(options = {}) {
10
+ return async (memberId, action) => {
11
+ const response = await (options.fetch ?? fetch)(`${appOrigin(options.environment ?? process.env)}/mcp`, {
12
+ method: "POST",
13
+ headers: {
14
+ authorization: `Bearer ${await (options.token ?? runtimeToken)()}`,
15
+ "X-SegmentStream-Act-As": memberId,
16
+ "content-type": "application/json",
17
+ accept: "application/json, text/event-stream",
18
+ "mcp-protocol-version": "2025-11-25"
19
+ },
20
+ body: JSON.stringify({
21
+ jsonrpc: "2.0",
22
+ id: "approval-tools",
23
+ method: "tools/list",
24
+ params: {}
25
+ }),
26
+ signal: AbortSignal.timeout(15e3),
27
+ redirect: "error"
28
+ });
29
+ if (!response.ok) throw new Error("Cannot check action permission.");
30
+ const body = await response.json();
31
+ const bareName = action.split("__").at(-1);
32
+ const tool = body.result?.tools?.find((candidate) => candidate.name === bareName);
33
+ if (tool === void 0) throw new Error("Action not permitted.");
34
+ return tool.annotations ?? {};
35
+ };
36
+ }
37
+ function actionToolName(entity, action) {
38
+ const snake = (name) => name.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
39
+ return `${snake(entity)}_${snake(action)}`;
40
+ }
41
+ export { actionToolName, ontologyToolLabels };
@@ -0,0 +1,14 @@
1
+ export interface PendingQuestion {
2
+ readonly id: string;
3
+ readonly question: string;
4
+ readonly task: string;
5
+ readonly memberId: string;
6
+ readonly chatId: string;
7
+ readonly messageThreadId: string;
8
+ readonly address: string;
9
+ }
10
+ /** Intent survives turns and compaction in the session, independently of model history. */
11
+ export declare const pendingQuestions: import("eve/context").StateHandle<Record<string, PendingQuestion>>;
12
+ export declare function questionKey(question: Pick<PendingQuestion, "memberId" | "chatId" | "messageThreadId">): string;
13
+ export declare function isPendingQuestion(value: unknown): value is PendingQuestion;
14
+ export declare function answerMarker(id: string): string;
@@ -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 the tail of a session's durable stream; tests pass a fake. */
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
- events: { "message.completed": replyDelivery(observed) }
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>);
@@ -11,6 +11,8 @@ var InboundRequest = class {
11
11
  files;
12
12
  sessions;
13
13
  waitUntil;
14
+ address;
15
+ respond;
14
16
  constructor(agentDescription) {
15
17
  this.agentDescription = agentDescription;
16
18
  }
@@ -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
- const address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
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") dependencies.observed.clear(chat);
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: telegramPrincipal({
255
- message: sent,
256
- member: lookup.member
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
- `Write in this chat${to}, ask what the request needs, and handle the replies here.`
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, as much of eve's envelope as the policy reads. */
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
- /** Visible assistant message content, also used to recover smart-routing context. */
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
- /** The last events of a session, oldest first. Production reads eve's durable stream. */
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",
@@ -0,0 +1,12 @@
1
+ declare const _default: import("eve/tools").ToolDefinition<Record<string, unknown>, {
2
+ pendingQuestion: {
3
+ id: string;
4
+ question: string;
5
+ task: string;
6
+ memberId: string;
7
+ chatId: string;
8
+ messageThreadId: string;
9
+ address: string;
10
+ };
11
+ }>;
12
+ export default _default;
@@ -0,0 +1,50 @@
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 } from "../lib/telegram/principal.mjs";
8
+ import { pendingQuestions, questionKey } from "../lib/questions.mjs";
9
+ import { stringArgument } from "../lib/attachment-tools.mjs";
10
+ import { defineTool } from "eve/tools";
11
+ var questions_ask_default = defineTool({
12
+ availableInSubagents: false,
13
+ description: "Ask for missing or ambiguous information needed to continue the member's task. Saves the task and question in the session, sends the question, and waits for their answer. Never use this for approval: destructive or irreversible actions have Confirm / Cancel buttons.",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ question: {
18
+ type: "string",
19
+ description: "The one question to send to the member."
20
+ },
21
+ task: {
22
+ type: "string",
23
+ description: "The pending task, including known targets and supplied values."
24
+ }
25
+ },
26
+ required: ["question", "task"],
27
+ additionalProperties: false
28
+ },
29
+ endsTurn: true,
30
+ execute(input, ctx) {
31
+ const memberId = memberIdOf(ctx.session.auth.current);
32
+ const chatId = ctx.session.auth.current?.attributes.chatId;
33
+ if (memberId === void 0 || typeof chatId !== "string") throw new Error("A member in a chat is required.");
34
+ const question = {
35
+ id: ctx.callId,
36
+ question: stringArgument(input, "question"),
37
+ task: stringArgument(input, "task"),
38
+ memberId,
39
+ chatId,
40
+ messageThreadId: String(ctx.session.auth.current?.attributes.messageThreadId ?? ""),
41
+ address: ""
42
+ };
43
+ pendingQuestions.update((all) => ({
44
+ ...all,
45
+ [questionKey(question)]: question
46
+ }));
47
+ return { pendingQuestion: question };
48
+ }
49
+ });
50
+ export { questions_ask_default as default };
@@ -5,3 +5,4 @@ export { default as attachments_list } from "../extension/tools/attachments_list
5
5
  export { default as attachments_promote } from "../extension/tools/attachments_promote.js";
6
6
  export { default as attachments_read } from "../extension/tools/attachments_read.js";
7
7
  export { default as attachments_table } from "../extension/tools/attachments_table.js";
8
+ export { default as questions_ask } from "../extension/tools/questions_ask.js";
@@ -9,4 +9,5 @@ import attachments_list_default from "../extension/tools/attachments_list.mjs";
9
9
  import attachments_promote_default from "../extension/tools/attachments_promote.mjs";
10
10
  import attachments_read_default from "../extension/tools/attachments_read.mjs";
11
11
  import attachments_table_default from "../extension/tools/attachments_table.mjs";
12
- export { attachments_discard_default as attachments_discard, attachments_list_default as attachments_list, attachments_promote_default as attachments_promote, attachments_read_default as attachments_read, attachments_table_default as attachments_table };
12
+ import questions_ask_default from "../extension/tools/questions_ask.mjs";
13
+ export { attachments_discard_default as attachments_discard, attachments_list_default as attachments_list, attachments_promote_default as attachments_promote, attachments_read_default as attachments_read, attachments_table_default as attachments_table, questions_ask_default as questions_ask };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",