@segmentstream/agent 0.1.3 → 0.1.5

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.
@@ -0,0 +1,84 @@
1
+ import type { TelegramMessage } from "eve/channels/telegram";
2
+ import type { SessionSettings } from "../../config.js";
3
+ /**
4
+ * Session policy for the Telegram channel (`agent-design.md`, section 4, and docs/agent.md,
5
+ * "Sessions"): what a group session spans, the daily compaction and the idle reset. The policy
6
+ * runs in the webhook route, before eve delivers a message, with the operations the route offers
7
+ * on a continuation address (`resolveSession`, `compact`, `clear`); the history it needs is read
8
+ * from the session's own durable stream, so nothing is stored beside eve.
9
+ */
10
+ /** eve's Telegram continuation address: `chat:thread:conversation`, the last two possibly empty. */
11
+ export interface TelegramAddress {
12
+ readonly chatId: string;
13
+ readonly messageThreadId: string;
14
+ readonly conversationId: string;
15
+ }
16
+ export declare function parseAddress(address: string): TelegramAddress;
17
+ export declare function formatAddress(address: TelegramAddress): string;
18
+ /**
19
+ * The address eve derives from an inbound message: the chat, the forum topic and, in a group, the
20
+ * reply thread (a reply to the bot continues the bot's message, anything else starts its own).
21
+ */
22
+ export declare function messageAddress(message: TelegramMessage): string;
23
+ /**
24
+ * The address a message continues under the configured scope. `chat` drops eve's reply-thread
25
+ * anchor, so every message of a group chat (and forum topic) reaches one session; `thread` keeps
26
+ * eve's own address. A private chat has no anchor either way.
27
+ */
28
+ export declare function scopedAddress(address: string, scope: SessionSettings["scope"]): string;
29
+ /** The most recent `HH:MM` in `timezone` at or before `now`. */
30
+ export declare function compactBoundary(now: Date, compactAt: string, timezone: string): Date;
31
+ /** What the policy knows about one session: from its stream, then kept per process. */
32
+ export interface SessionHistory {
33
+ /** When the session last did anything. */
34
+ readonly lastAt: number;
35
+ /** When its history last started afresh: creation, compaction or clear. */
36
+ readonly resetAt: number;
37
+ /** Turns completed since `resetAt`; a compaction is worth nothing before the first. */
38
+ readonly turnsSinceReset: number;
39
+ }
40
+ /** One recorded event, as much of eve's envelope as the policy reads. */
41
+ export interface RecordedEvent {
42
+ readonly type: string;
43
+ readonly meta: {
44
+ readonly at: string;
45
+ };
46
+ }
47
+ /** The last events of a session, oldest first. Production reads eve's durable stream. */
48
+ export type HistoryReader = (sessionId: string) => Promise<ReadonlyArray<RecordedEvent>>;
49
+ /** How many events of the tail the reader fetches: enough for a day of a busy group. */
50
+ export declare const HISTORY_TAIL = 400;
51
+ /** Folds a tail into a history; with no reset marker in it, `resetAt` is its earliest event. */
52
+ export declare function historyOf(events: ReadonlyArray<RecordedEvent>): SessionHistory;
53
+ /** The route's operations on a continuation address, as eve gives them to the handler. */
54
+ export interface SessionOperations {
55
+ readonly resolveSession: (address: string) => Promise<{
56
+ readonly id: string;
57
+ } | undefined>;
58
+ readonly from: (address: string) => {
59
+ compact(): Promise<unknown>;
60
+ clear(): Promise<unknown>;
61
+ };
62
+ }
63
+ export type SessionAction = "none" | "cleared" | "compacted";
64
+ export interface SessionPolicy {
65
+ /**
66
+ * Applies the policy to the session `address` continues, before a message is delivered to it:
67
+ * clears a history idle for longer than `idleResetHours`, else compacts it once after each
68
+ * day's `compactAt`. A session that does not exist yet needs neither.
69
+ */
70
+ apply(input: {
71
+ readonly address: string;
72
+ readonly operations: SessionOperations;
73
+ /** The newest activity the stream does not show, such as an observed group message. */
74
+ readonly activeAt?: number;
75
+ readonly now?: Date;
76
+ }): Promise<SessionAction>;
77
+ /** Records that a message was just delivered to the session. */
78
+ delivered(address: string, now?: Date): void;
79
+ }
80
+ export declare function sessionPolicy(input: {
81
+ readonly settings: () => SessionSettings;
82
+ readonly read: HistoryReader;
83
+ readonly log?: (message: string, error: unknown) => void;
84
+ }): SessionPolicy;
@@ -0,0 +1,200 @@
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 parseAddress(address) {
8
+ const [chatId = "", messageThreadId = "", conversationId = ""] = address.split(":");
9
+ return {
10
+ chatId,
11
+ messageThreadId,
12
+ conversationId
13
+ };
14
+ }
15
+ function formatAddress(address) {
16
+ return `${address.chatId}:${address.messageThreadId}:${address.conversationId}`;
17
+ }
18
+ function messageAddress(message) {
19
+ const group = message.chat.type === "group" || message.chat.type === "supergroup";
20
+ const reply = message.replyToMessage;
21
+ const conversationId = !group ? "" : reply?.from?.isBot === true ? reply.messageId : message.messageId;
22
+ return formatAddress({
23
+ chatId: message.chat.id,
24
+ messageThreadId: message.messageThreadId === void 0 ? "" : String(message.messageThreadId),
25
+ conversationId
26
+ });
27
+ }
28
+ function scopedAddress(address, scope) {
29
+ if (scope === "thread") return address;
30
+ return formatAddress({
31
+ ...parseAddress(address),
32
+ conversationId: ""
33
+ });
34
+ }
35
+ function localParts(timezone, date) {
36
+ const parts = new Intl.DateTimeFormat("en-US", {
37
+ timeZone: timezone,
38
+ hourCycle: "h23",
39
+ year: "numeric",
40
+ month: "2-digit",
41
+ day: "2-digit",
42
+ hour: "2-digit",
43
+ minute: "2-digit",
44
+ second: "2-digit"
45
+ }).formatToParts(date);
46
+ const value = (type) => Number(parts.find((part) => part.type === type)?.value ?? "0");
47
+ return {
48
+ year: value("year"),
49
+ month: value("month"),
50
+ day: value("day"),
51
+ hour: value("hour"),
52
+ minute: value("minute"),
53
+ second: value("second")
54
+ };
55
+ }
56
+ function offsetMinutes(timezone, date) {
57
+ const local = localParts(timezone, date);
58
+ const asUtc = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute, local.second);
59
+ return Math.round((asUtc - date.getTime()) / 6e4);
60
+ }
61
+ function instantOf(timezone, local) {
62
+ const guess = Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute);
63
+ const first = new Date(guess - offsetMinutes(timezone, new Date(guess)) * 6e4);
64
+ const second = new Date(guess - offsetMinutes(timezone, first) * 6e4);
65
+ const back = localParts(timezone, second);
66
+ if (back.hour !== local.hour || back.minute !== local.minute) return first > second ? first : second;
67
+ return second;
68
+ }
69
+ function compactBoundary(now, compactAt, timezone) {
70
+ const [hour = 0, minute = 0] = compactAt.split(":").map(Number);
71
+ const today = localParts(timezone, now);
72
+ const candidate = instantOf(timezone, {
73
+ ...today,
74
+ hour,
75
+ minute
76
+ });
77
+ if (candidate.getTime() <= now.getTime()) return candidate;
78
+ const yesterday = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
79
+ return instantOf(timezone, {
80
+ year: yesterday.getUTCFullYear(),
81
+ month: yesterday.getUTCMonth() + 1,
82
+ day: yesterday.getUTCDate(),
83
+ hour,
84
+ minute
85
+ });
86
+ }
87
+ const HISTORY_TAIL = 400;
88
+ function historyOf(events) {
89
+ let lastAt = 0;
90
+ let resetAt = 0;
91
+ let earliest = 0;
92
+ let marked = false;
93
+ let turnsSinceReset = 0;
94
+ for (const event of events) {
95
+ const at = Date.parse(event.meta.at);
96
+ if (Number.isNaN(at)) continue;
97
+ lastAt = Math.max(lastAt, at);
98
+ earliest = earliest === 0 ? at : Math.min(earliest, at);
99
+ if (event.type === "session.started" || event.type === "compaction.completed" || event.type === "context.cleared") {
100
+ resetAt = at;
101
+ marked = true;
102
+ turnsSinceReset = 0;
103
+ } else if (event.type === "turn.completed") turnsSinceReset += 1;
104
+ }
105
+ return {
106
+ lastAt,
107
+ resetAt: marked ? resetAt : earliest,
108
+ turnsSinceReset
109
+ };
110
+ }
111
+ const MAXIMUM_REMEMBERED = 500;
112
+ function sessionPolicy(input) {
113
+ const remembered = new Map();
114
+ const log = input.log ?? ((message, error) => console.warn(message, error));
115
+ function remember(address, history) {
116
+ remembered.delete(address);
117
+ remembered.set(address, history);
118
+ if (remembered.size > MAXIMUM_REMEMBERED) {
119
+ const oldest = remembered.keys().next();
120
+ if (oldest.done !== true) remembered.delete(oldest.value);
121
+ }
122
+ }
123
+ async function history(address, operations, fresh = false) {
124
+ const known = fresh ? void 0 : remembered.get(address);
125
+ if (known !== void 0) return known;
126
+ const session = await operations.resolveSession(address);
127
+ if (session === void 0) {
128
+ remembered.delete(address);
129
+ return;
130
+ }
131
+ const read = historyOf(await input.read(session.id));
132
+ remember(address, read);
133
+ return read;
134
+ }
135
+ function due(known, settings, now, activeAt) {
136
+ const idle = settings.idleResetHours;
137
+ const lastAt = Math.max(known.lastAt, activeAt);
138
+ if (idle !== false && now.getTime() - lastAt >= idle * 36e5) return "clear";
139
+ if (settings.compactAt !== false) {
140
+ const boundary = compactBoundary(now, settings.compactAt, settings.timezone).getTime();
141
+ if (known.resetAt < boundary && known.turnsSinceReset > 0) return "compact";
142
+ }
143
+ return "none";
144
+ }
145
+ return {
146
+ async apply({ address, operations, activeAt = 0, now = new Date() }) {
147
+ const settings = input.settings();
148
+ const cached = remembered.has(address);
149
+ let known;
150
+ try {
151
+ known = await history(address, operations);
152
+ } catch (error) {
153
+ log("The session policy could not read the session's history; delivering as is.", error);
154
+ return "none";
155
+ }
156
+ if (known === void 0) return "none";
157
+ let action = due(known, settings, now, activeAt);
158
+ if (action !== "none" && cached) {
159
+ try {
160
+ known = await history(address, operations, true);
161
+ } catch (error) {
162
+ log("The session policy could not re-read the session's history; delivering as is.", error);
163
+ return "none";
164
+ }
165
+ if (known === void 0) return "none";
166
+ action = due(known, settings, now, activeAt);
167
+ }
168
+ if (action === "clear") try {
169
+ await operations.from(address).clear();
170
+ remember(address, {
171
+ lastAt: known.lastAt,
172
+ resetAt: now.getTime(),
173
+ turnsSinceReset: 0
174
+ });
175
+ return "cleared";
176
+ } catch (error) {
177
+ log("The idle reset could not clear the session; delivering as is.", error);
178
+ return "none";
179
+ }
180
+ if (action === "compact") try {
181
+ await operations.from(address).compact();
182
+ remembered.delete(address);
183
+ return "compacted";
184
+ } catch (error) {
185
+ log("The daily compaction could not be queued; delivering as is.", error);
186
+ }
187
+ return "none";
188
+ },
189
+ delivered(address, now = new Date()) {
190
+ const known = remembered.get(address);
191
+ if (known === void 0) return;
192
+ remember(address, {
193
+ lastAt: now.getTime(),
194
+ resetAt: known.resetAt,
195
+ turnsSinceReset: known.turnsSinceReset + 1
196
+ });
197
+ }
198
+ };
199
+ }
200
+ export { HISTORY_TAIL, compactBoundary, formatAddress, historyOf, messageAddress, parseAddress, scopedAddress, sessionPolicy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -61,7 +61,8 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@vercel/oidc": "^3.8.4",
64
- "ai": "^7.0.128"
64
+ "ai": "^7.0.128",
65
+ "jose": "^6.2.12"
65
66
  },
66
67
  "peerDependencies": {
67
68
  "eve": "0.76.3"