@segmentstream/agent 0.6.7 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -381,11 +381,25 @@ Apache-2.0
381
381
 
382
382
  Production Telegram webhooks archive every receivable message in the platform database,
383
383
  including automated posts and unanswered messages. The extension also records replies,
384
- reply/silent decisions, tool names and redacted argument summaries, results' status, and
384
+ reply/silent decisions, tool names, redacted arguments/results/errors, and
385
385
  attachment references. It reloads stored context for the same chat/topic each turn, including
386
- across cold starts and compaction. File bytes and tool output bodies are not archived.
387
-
388
- Retention defaults to 365 days per workspace. Platform REST reads, the workspace ontology's
386
+ across cold starts and compaction. File bytes are replaced by markers.
387
+
388
+ The Eve hook sanitizes tool arguments, results and failure messages in `argsText`,
389
+ `resultText` and `errorText`; ingestion re-sanitizes them. UTF-8 caps are 16/32/4 KiB, with explicit JSON
390
+ truncation markers. Content and argument summaries expire within 30 days of the original event
391
+ timestamp (clamped to server time); replay cannot renew that window after archive deletion.
392
+ Embedded JSON objects and arrays are sanitized recursively. Strings under text/content/body/csv/tsv
393
+ and file/email body fields are omitted unless they parse as JSON objects or arrays. This includes
394
+ all CSV/TSV documents, with or without headers. `argsSummary` keeps its pre-log semantics: the SDK
395
+ applies bounded credential and file-byte redaction, and ingestion preserves the supplied summary.
396
+ Deploy backend migration 0043/API before upgrading to agent SDK 0.6.10.
397
+ `segmentstream agent logs` and its `show`/`call` commands read this content
398
+ with a human Cloud session; all members with workspace access can read DMs too.
399
+ Runtime/model/MCP conversation readers never receive these new fields.
400
+
401
+ Archive metadata and message text retain the workspace policy (365 days by default).
402
+ Platform REST reads, the workspace ontology's
389
403
  `chat_history` and `chat_search`, and the member REST facade enforce workspace/group
390
404
  permissions and DM participant/admin scope. Deploy the backend migration/API and upgrade the
391
405
  ontology SDK along with this extension. See the repository's
@@ -0,0 +1,212 @@
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 "../extension/lib/telegram/principal.mjs";
8
+ import { conversationEvent, conversations, summarizeArguments } from "../extension/lib/conversations.mjs";
9
+ import { parseAddress } from "../extension/lib/telegram/sessions.mjs";
10
+ import { completionKind } from "../extension/lib/telegram/silent.mjs";
11
+ const CONTENT_CAPS = {
12
+ args: 16384,
13
+ result: 32768,
14
+ error: 4096
15
+ };
16
+ const sensitive = /token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn|email|phone|address|birth|document.?text|raw.?content|body|bytes|base64/i;
17
+ const encoder = new TextEncoder();
18
+ function luhn(value) {
19
+ const digits = value.replace(/\D/g, "");
20
+ if (digits.length < 13 || digits.length > 19) return false;
21
+ let sum = 0;
22
+ let double = false;
23
+ for (let i = digits.length - 1; i >= 0; i--) {
24
+ let n = Number(digits[i]);
25
+ if (double && (n *= 2) > 9) n -= 9;
26
+ sum += n;
27
+ double = !double;
28
+ }
29
+ return sum % 10 === 0;
30
+ }
31
+ function redactText(value) {
32
+ return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "[binary data]").replace(/-----BEGIN [^-\r\n]+-----[\s\S]*?(?:-----END [^-\r\n]+-----|$)/g, "[redacted PEM]").replace(/data:[^\s"']+/gi, "[binary data]").replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9+/_=.:~\-]+/gi, "[redacted authorization]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted JWT]").replace(/\b(?:ssw_|sscli_|sk[-_]|rk[-_])[A-Za-z0-9._~\-]+/gi, "[redacted key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@").replace(/([?&](?:[^=&#\s]*(?:token|password|secret|key|signature|credential|session|auth|cookie)[^=&#\s]*|sig)=)[^&#\s"']*/gi, "$1[redacted]").replace(/((?:["']?)(?:[\w-]*(?:token|password|passwd|secret|authorization|cookie|credential|api.?key|private.?key|session|iban|card|cvv|ssn)[\w-]*)(?:["']?)\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;})]+)/gi, "$1[redacted]").replace(/\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]){11,30}\b/gi, "[redacted IBAN]").replace(/\b\d(?:[ -]?\d){12,18}\b/g, (candidate) => luhn(candidate) ? "[redacted card]" : candidate).replace(/[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9.-]*[A-Z0-9])?\.[A-Z]{2,}/gi, "[email]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[redacted SSN]").replace(/\b[A-Za-z0-9+/]{128,}={0,2}/g, "[binary data]");
33
+ }
34
+ const documentField = /^(?:text|content|body|csv|tsv|file[_-]?body|email[_-]?body)$/i;
35
+ const omittedDocument = "[omitted: unstructured document content]";
36
+ function redactContent(value) {
37
+ const seen = new WeakSet();
38
+ let nodes = 0;
39
+ const visit = (item, depth, field) => {
40
+ if (++nodes > 5e3 || depth > 12) return "[omitted: content complexity]";
41
+ if (typeof item === "string") {
42
+ try {
43
+ const embedded = JSON.parse(item);
44
+ if (embedded !== null && typeof embedded === "object") return JSON.stringify(visit(embedded, depth + 1, field));
45
+ } catch {}
46
+ if (documentField.test(field ?? "")) return omittedDocument;
47
+ return redactText(item);
48
+ }
49
+ if (typeof item === "bigint") return item.toString();
50
+ if (item === null || typeof item !== "object") return item;
51
+ if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer) return "[binary data]";
52
+ if (seen.has(item)) return "[omitted: circular]";
53
+ seen.add(item);
54
+ if (Array.isArray(item)) {
55
+ const entries = item.slice(0, 200).map((entry) => visit(entry, depth + 1, field));
56
+ if (item.length > 200) entries.push("[omitted: array entries]");
57
+ return entries;
58
+ }
59
+ const entries = Object.entries(item);
60
+ return Object.fromEntries([...entries.slice(0, 200).map(([key, entry]) => [redactText(key), sensitive.test(key) && !documentField.test(key) ? "[redacted]" : visit(entry, depth + 1, key)]), ...entries.length > 200 ? [["_omitted", "[omitted: object entries]"]] : []]);
61
+ };
62
+ return visit(value, 0);
63
+ }
64
+ function contentText(value, cap) {
65
+ if (value == null) return null;
66
+ let parsed = value;
67
+ if (typeof value === "string") try {
68
+ parsed = JSON.parse(value);
69
+ } catch {}
70
+ const redacted = redactContent(parsed);
71
+ const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted) ?? "[omitted]";
72
+ if (encoder.encode(text).length <= cap) return text;
73
+ let preview = text;
74
+ while (true) {
75
+ const result = JSON.stringify({
76
+ truncated: true,
77
+ preview
78
+ });
79
+ const bytes = encoder.encode(result).length;
80
+ if (bytes <= cap) return result;
81
+ const available = Math.max(0, encoder.encode(preview).length - (bytes - cap) - 4);
82
+ preview = new TextDecoder().decode(encoder.encode(preview).slice(0, available), { stream: true });
83
+ }
84
+ }
85
+ function conversationCoordinates(context) {
86
+ const principal = context.session.auth.current;
87
+ const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
88
+ const address = context.channel.continuationToken;
89
+ const attributes = principal?.attributes;
90
+ const fields = address?.split(":");
91
+ const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
92
+ const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
93
+ const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
94
+ const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
95
+ if (!chatId) return;
96
+ const memberId = memberIdOf(principal);
97
+ const chatType = attributes?.chatType;
98
+ return {
99
+ provider: telegram ? "telegram" : context.channel.kind ?? "eve",
100
+ chatId,
101
+ chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
102
+ threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
103
+ participantIds: memberId === void 0 ? [] : [memberId],
104
+ senderId: principal?.subject ?? null
105
+ };
106
+ }
107
+ function conversationRecorder(archive = conversations()) {
108
+ return async (event, context) => {
109
+ const coordinates = conversationCoordinates(context);
110
+ if (coordinates === void 0) return;
111
+ const base = {
112
+ ...coordinates,
113
+ sessionId: context.session.id,
114
+ turnId: event.data !== void 0 && "turnId" in event.data ? event.data.turnId : context.session.turn.id,
115
+ at: event.meta.at
116
+ };
117
+ const eventId = `eve:${context.session.id}:${event.meta.id}`;
118
+ if (event.type === "actions.requested") for (const action of event.data.actions) {
119
+ const name = "toolName" in action ? action.toolName : action.name;
120
+ await archive.append(conversationEvent({
121
+ ...base,
122
+ eventId: `${eventId}:${action.callId}`,
123
+ kind: "tool_call",
124
+ toolCallId: action.callId,
125
+ toolName: name,
126
+ argsSummary: summarizeArguments(action.input),
127
+ argsText: contentText(action.input, CONTENT_CAPS.args),
128
+ status: "requested"
129
+ }));
130
+ }
131
+ else if (event.type === "action.result") {
132
+ const result = event.data.result;
133
+ const name = "toolName" in result ? result.toolName : "name" in result ? result.name : result.subagentName;
134
+ await archive.append(conversationEvent({
135
+ ...base,
136
+ eventId,
137
+ kind: "tool_result",
138
+ toolCallId: result.callId,
139
+ toolName: name,
140
+ status: event.data.status,
141
+ resultText: contentText("output" in result ? result.output : null, CONTENT_CAPS.result),
142
+ errorText: contentText(event.data.error?.message ?? (event.data.status === "failed" ? resultErrorMessage(result.output) : null), CONTENT_CAPS.error)
143
+ }));
144
+ } else if (event.type === "task.started" || event.type === "task.settled") await archive.append(conversationEvent({
145
+ ...base,
146
+ eventId,
147
+ kind: event.type === "task.started" ? "tool_call" : "tool_result",
148
+ toolCallId: event.data.callId,
149
+ toolName: event.data.name ?? "task",
150
+ argsSummary: "{}",
151
+ ...event.type === "task.settled" ? {
152
+ resultText: contentText(event.data.output, CONTENT_CAPS.result),
153
+ errorText: contentText(event.data.error?.message, CONTENT_CAPS.error)
154
+ } : {},
155
+ status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
156
+ }));
157
+ else if (event.type === "turn.failed" || event.type === "step.failed") await archive.append(conversationEvent({
158
+ ...base,
159
+ turnId: event.data.turnId,
160
+ eventId,
161
+ kind: "decision",
162
+ decision: "silent",
163
+ reason: event.type,
164
+ status: "failed",
165
+ errorText: contentText(event.data.message, CONTENT_CAPS.error)
166
+ }));
167
+ else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
168
+ const kind = completionKind(event.data.message, event.data.finishReason);
169
+ const silent = kind !== "reply";
170
+ const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
171
+ await archive.append(conversationEvent({
172
+ ...base,
173
+ eventId,
174
+ kind: "decision",
175
+ decision: silent ? "silent" : "reply",
176
+ reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
177
+ text: note,
178
+ argsSummary: JSON.stringify({
179
+ outputKind: kind,
180
+ outputCharacters: event.data.message?.length ?? 0,
181
+ finishReason: event.data.finishReason
182
+ })
183
+ }));
184
+ if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
185
+ ...base,
186
+ eventId: `${eventId}:outbound`,
187
+ kind: "outbound",
188
+ text: event.data.message,
189
+ senderId: "agent",
190
+ senderIsBot: true,
191
+ status: "completed"
192
+ }));
193
+ } else if (event.type === "message.received" && coordinates.provider !== "telegram") await archive.append(conversationEvent({
194
+ ...base,
195
+ eventId,
196
+ kind: "inbound",
197
+ text: event.data.message,
198
+ attachments: (event.data.parts ?? []).filter((part) => part.type === "file").map((part, index) => ({
199
+ id: `${eventId}:file:${index}`,
200
+ kind: "file",
201
+ ...part.filename ? { filename: part.filename } : {},
202
+ contentType: part.mediaType
203
+ }))
204
+ }));
205
+ };
206
+ }
207
+ function resultErrorMessage(output) {
208
+ if (typeof output === "string") return output;
209
+ if (output !== null && typeof output === "object" && "message" in output && typeof output.message === "string") return output.message;
210
+ return null;
211
+ }
212
+ export { conversationRecorder as n, conversationCoordinates as t };
@@ -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 { conversationRecorder } from "../lib/conversation-runtime.mjs";
7
+ import { n as conversationRecorder } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
8
8
  import { defineHook } from "eve/hooks";
9
9
  var conversations_default = defineHook({ events: { "*": conversationRecorder() } });
10
10
  export { conversations_default as default };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { conversations, storedConversationContext } from "../lib/conversations.mjs";
8
- import { conversationCoordinates } from "../lib/conversation-runtime.mjs";
8
+ import { t as conversationCoordinates } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
9
9
  import { defineDynamic, defineInstructions } from "eve/instructions";
10
10
  const archive = conversations();
11
11
  var conversations_default = defineDynamic({ events: { "turn.started": async (_event, context) => {
@@ -57,7 +57,7 @@ function describeChatPost(input) {
57
57
  title: target.title,
58
58
  attribution: caller.attribution,
59
59
  text: values.text,
60
- finalText: `${converter.fromMarkdown(values.text)}\n\n_${footer}_`,
60
+ finalText: `${converter.fromMarkdown(values.text)}\n\n──────────\n_${footer}_`,
61
61
  replyTo: typeof values.reply_to === "string" ? values.reply_to : null
62
62
  }
63
63
  };
@@ -4,113 +4,5 @@ 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 { memberIdOf } from "./telegram/principal.mjs";
8
- import { conversationEvent, conversations, summarizeArguments } from "./conversations.mjs";
9
- import { parseAddress } from "./telegram/sessions.mjs";
10
- import { completionKind } from "./telegram/silent.mjs";
11
- function conversationCoordinates(context) {
12
- const principal = context.session.auth.current;
13
- const telegram = principal?.authenticator === "segmentstream-telegram" || context.channel.kind?.includes("telegram");
14
- const address = context.channel.continuationToken;
15
- const attributes = principal?.attributes;
16
- const fields = address?.split(":");
17
- const localAddress = telegram && fields?.length === 4 ? fields.slice(1).join(":") : address;
18
- const parsed = telegram && localAddress !== void 0 && /^-?\d+:\d*:\d*$/u.test(localAddress) ? parseAddress(localAddress) : void 0;
19
- const attributeChatId = typeof attributes?.chatId === "string" && (!telegram || /^-?\d+$/u.test(attributes.chatId)) ? attributes.chatId : void 0;
20
- const chatId = telegram ? parsed?.chatId ?? attributeChatId : attributeChatId !== void 0 ? attributeChatId : address;
21
- if (!chatId) return;
22
- const memberId = memberIdOf(principal);
23
- const chatType = attributes?.chatType;
24
- return {
25
- provider: telegram ? "telegram" : context.channel.kind ?? "eve",
26
- chatId,
27
- chatType: chatType === "group" || chatType === "supergroup" || chatType === void 0 && telegram && chatId.startsWith("-") ? "group" : chatType === "channel" ? "channel" : "private",
28
- threadId: !telegram ? null : parsed !== void 0 ? parsed.messageThreadId || null : typeof attributes?.messageThreadId === "string" ? attributes.messageThreadId : null,
29
- participantIds: memberId === void 0 ? [] : [memberId],
30
- senderId: principal?.subject ?? null
31
- };
32
- }
33
- function conversationRecorder(archive = conversations()) {
34
- return async (event, context) => {
35
- const coordinates = conversationCoordinates(context);
36
- if (coordinates === void 0) return;
37
- const base = {
38
- ...coordinates,
39
- sessionId: context.session.id,
40
- turnId: context.session.turn.id,
41
- at: event.meta.at
42
- };
43
- const eventId = `eve:${context.session.id}:${event.meta.id}`;
44
- if (event.type === "actions.requested") for (const action of event.data.actions) {
45
- const name = "toolName" in action ? action.toolName : action.name;
46
- await archive.append(conversationEvent({
47
- ...base,
48
- eventId: `${eventId}:${action.callId}`,
49
- kind: "tool_call",
50
- toolCallId: action.callId,
51
- toolName: name,
52
- argsSummary: summarizeArguments(action.input),
53
- status: "requested"
54
- }));
55
- }
56
- else if (event.type === "action.result") {
57
- const result = event.data.result;
58
- const name = "toolName" in result ? result.toolName : "name" in result ? result.name : result.kind;
59
- await archive.append(conversationEvent({
60
- ...base,
61
- eventId,
62
- kind: "tool_result",
63
- toolCallId: result.callId,
64
- toolName: name,
65
- status: event.data.status
66
- }));
67
- } else if (event.type === "task.started" || event.type === "task.settled") await archive.append(conversationEvent({
68
- ...base,
69
- eventId,
70
- kind: event.type === "task.started" ? "tool_call" : "tool_result",
71
- toolCallId: event.data.callId,
72
- toolName: event.data.name ?? "task",
73
- argsSummary: "{}",
74
- status: event.type === "task.started" ? "requested" : event.data.status === "cancelled" ? "cancelled" : event.data.status === "failed" ? "failed" : "completed"
75
- }));
76
- else if (event.type === "message.completed" && event.data.finishReason !== "tool-calls") {
77
- const kind = completionKind(event.data.message, event.data.finishReason);
78
- const silent = kind !== "reply";
79
- const note = kind === "silent_marker" ? "Model returned an explicit silence marker; no message delivery." : kind === "empty" ? "Model completed without final text; no message delivery." : kind === "whitespace" ? "Model returned only whitespace; no message delivery." : "Model produced a reply; transport delivery is recorded separately.";
80
- await archive.append(conversationEvent({
81
- ...base,
82
- eventId,
83
- kind: "decision",
84
- decision: silent ? "silent" : "reply",
85
- reason: kind === "reply" ? "model_reply" : `model_${kind}_output`,
86
- text: note,
87
- argsSummary: JSON.stringify({
88
- outputKind: kind,
89
- outputCharacters: event.data.message?.length ?? 0,
90
- finishReason: event.data.finishReason
91
- })
92
- }));
93
- if (coordinates.provider !== "telegram" && !silent) await archive.append(conversationEvent({
94
- ...base,
95
- eventId: `${eventId}:outbound`,
96
- kind: "outbound",
97
- text: event.data.message,
98
- senderId: "agent",
99
- senderIsBot: true,
100
- status: "completed"
101
- }));
102
- } else if (event.type === "message.received" && coordinates.provider !== "telegram") await archive.append(conversationEvent({
103
- ...base,
104
- eventId,
105
- kind: "inbound",
106
- text: event.data.message,
107
- attachments: (event.data.parts ?? []).filter((part) => part.type === "file").map((part, index) => ({
108
- id: `${eventId}:file:${index}`,
109
- kind: "file",
110
- ...part.filename ? { filename: part.filename } : {},
111
- contentType: part.mediaType
112
- }))
113
- }));
114
- };
115
- }
7
+ import { n as conversationRecorder, t as conversationCoordinates } from "../../_chunks/conversation-runtime-ovs8IhXJ.mjs";
116
8
  export { conversationCoordinates, conversationRecorder };
@@ -1,6 +1,6 @@
1
1
  import { type Environment } from "./environment.js";
2
2
  import { type RuntimeToken } from "./runtime-token.js";
3
- /** Provider-neutral platform archive. Only references and summaries cross this boundary. */
3
+ /** Provider-neutral platform archive. Content uses the shared redaction and byte-limit policy. */
4
4
  export interface ConversationEvent {
5
5
  readonly eventId: string;
6
6
  readonly provider: string;
@@ -22,6 +22,9 @@ export interface ConversationEvent {
22
22
  readonly toolCallId: string | null;
23
23
  readonly toolName: string | null;
24
24
  readonly argsSummary: string | null;
25
+ readonly argsText?: string | null;
26
+ readonly resultText?: string | null;
27
+ readonly errorText?: string | null;
25
28
  readonly status: "requested" | "completed" | "failed" | "rejected" | "cancelled" | "unknown" | "sent" | "uncertain" | null;
26
29
  readonly operationId?: string | null;
27
30
  readonly requestedBy?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.6.7",
3
+ "version": "0.6.10",
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",