@segmentstream/agent 0.16.0 → 0.17.0

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 (41) hide show
  1. package/README.md +64 -1
  2. package/dist/_chunks/{build-metadata-CIFPiRho.mjs → build-metadata-DR4gACLZ.mjs} +1 -1
  3. package/dist/extension/config.d.ts +14 -0
  4. package/dist/extension/config.mjs +14 -0
  5. package/dist/extension/hooks/sandbox.d.ts +9 -0
  6. package/dist/extension/hooks/sandbox.mjs +12 -0
  7. package/dist/extension/instructions/sandbox.d.ts +2 -0
  8. package/dist/extension/instructions/sandbox.mjs +36 -0
  9. package/dist/extension/lib/attachment-formats.d.ts +2 -0
  10. package/dist/extension/lib/attachment-formats.mjs +29 -1
  11. package/dist/extension/lib/attachments.d.ts +4 -0
  12. package/dist/extension/lib/attachments.mjs +1 -1
  13. package/dist/extension/lib/build-metadata.mjs +1 -1
  14. package/dist/extension/lib/conversation-runtime.d.ts +5 -1
  15. package/dist/extension/lib/conversation-runtime.mjs +31 -5
  16. package/dist/extension/lib/conversations.d.ts +6 -0
  17. package/dist/extension/lib/conversations.mjs +4 -1
  18. package/dist/extension/lib/ontology-arguments.d.ts +28 -0
  19. package/dist/extension/lib/ontology-arguments.mjs +54 -0
  20. package/dist/extension/lib/sandbox-session.d.ts +104 -0
  21. package/dist/extension/lib/sandbox-session.mjs +142 -0
  22. package/dist/extension/lib/sandbox.d.ts +38 -0
  23. package/dist/extension/lib/sandbox.mjs +64 -0
  24. package/dist/extension/lib/telegram/adapter.mjs +11 -4
  25. package/dist/extension/lib/telegram/conversations.d.ts +16 -0
  26. package/dist/extension/lib/telegram/conversations.mjs +60 -15
  27. package/dist/extension/lib/telegram/health.mjs +1 -1
  28. package/dist/extension/lib/telegram/lifecycle.mjs +1 -1
  29. package/dist/extension/lib/telegram/silent.d.ts +24 -2
  30. package/dist/extension/lib/telegram/silent.mjs +30 -4
  31. package/dist/extension/lib/telegram/turn-errors.d.ts +7 -1
  32. package/dist/extension/lib/telegram/turn-errors.mjs +89 -76
  33. package/dist/extension/tools/attachments_promote.d.ts +2 -7
  34. package/dist/extension/tools/attachments_promote.mjs +34 -10
  35. package/dist/extension/tools/ontology.d.ts +4 -3
  36. package/dist/extension/tools/ontology.mjs +10 -3
  37. package/dist/extension/tools/sandbox.d.ts +7 -0
  38. package/dist/extension/tools/sandbox.mjs +29 -0
  39. package/dist/tools/index.d.ts +1 -0
  40. package/dist/tools/index.mjs +2 -1
  41. package/package.json +6 -2
@@ -0,0 +1,104 @@
1
+ import { type StateHandle } from "eve/context";
2
+ import type { RuntimeSandboxSession, SandboxSession } from "eve/sandbox";
3
+ /**
4
+ * The SDK's side of a session's sandbox (`docs/agent.md`, "Sandbox"): the marker its selector
5
+ * writes, the session state recording when it opened, the gate every file tool passes and the
6
+ * cleanup. eve cannot say which environment a session's sandbox came from without opening it, and
7
+ * the configured environment object need not survive bundling, so the SDK's selector writes a
8
+ * marker into the sandbox it opens and the tools refuse a sandbox without one. The marker guards
9
+ * against a missing or replaced `agent/agent/sandbox.ts`; it is not a boundary against the model,
10
+ * which can only reach the sandbox after the gate has passed.
11
+ */
12
+ /** Where the SDK's selector records that it opened the sandbox. */
13
+ export declare const SANDBOX_MARKER_PATH = "/workspace/.segmentstream/sandbox.json";
14
+ /** Where eve stages the files that reach the model (`eve/docs/sandbox`, "Inbound attachments"). */
15
+ export declare const SANDBOX_ATTACHMENTS_DIRECTORY = "/workspace/.eve/attachments";
16
+ /** A sandbox older than this is replaced on its first file-tool use in a turn. */
17
+ export declare const SANDBOX_MAXIMUM_AGE_MS: number;
18
+ /** What a replacement carries over of the attachments staged within the last day. */
19
+ export declare const CARRIED_ATTACHMENTS: {
20
+ readonly files: 32;
21
+ readonly bytes: number;
22
+ };
23
+ export interface SandboxMarker {
24
+ readonly version: 1;
25
+ readonly sessionId: string;
26
+ /** When the selector opened the sandbox, as an ISO timestamp. */
27
+ readonly openedAt: string;
28
+ /** The toolset version the environment's snapshot was prepared with. */
29
+ readonly toolset: string;
30
+ readonly network: "deny-all";
31
+ }
32
+ /** What the session remembers about its sandbox, so cleanup never opens one just to delete it. */
33
+ export interface SandboxRecord {
34
+ readonly openedAt: string;
35
+ /** The turn whose first file-tool call last passed the gate. */
36
+ readonly checkedTurn?: string;
37
+ }
38
+ export declare const sandboxRecord: StateHandle<SandboxRecord | null>;
39
+ /** The session's sandbox did not come from `@segmentstream/agent/sandbox`; the tools fail closed. */
40
+ export declare class SandboxUnavailableError extends Error {
41
+ readonly name = "SandboxUnavailableError";
42
+ }
43
+ /** The part of an eve tool or hook context the sandbox functions use. */
44
+ export interface SandboxContext {
45
+ readonly session: {
46
+ readonly id: string;
47
+ readonly turn: {
48
+ readonly id: string;
49
+ };
50
+ };
51
+ getSandbox(): Promise<RuntimeSandboxSession>;
52
+ }
53
+ /** Parses a marker file; `undefined` for anything the SDK's selector would not have written. */
54
+ export declare function parseSandboxMarker(bytes: Uint8Array | null): SandboxMarker | undefined;
55
+ /**
56
+ * Initializes a sandbox the SDK's selector just opened: writes the marker and records the opening
57
+ * in session state. Outside an eve step there is no session state; the marker still gates the
58
+ * tools and the age check still runs on first use.
59
+ */
60
+ export declare function initializeSandbox(sandbox: SandboxSession, input: {
61
+ readonly sessionId: string;
62
+ readonly toolset: string;
63
+ readonly now?: Date;
64
+ readonly record?: StateHandle<SandboxRecord | null>;
65
+ }): Promise<SandboxMarker>;
66
+ /**
67
+ * The session's sandbox, once it has passed the gate: it must carry the SDK's marker for this
68
+ * session, so the file tools never run in eve's default sandbox, whose network is open. The first
69
+ * call in each turn checks the marker; a sandbox older than a day is deleted through its handle
70
+ * and reopened, carrying over the attachments staged within the last day so that the files of the
71
+ * current turn stay where eve's references point.
72
+ */
73
+ export declare function sessionSandbox(context: SandboxContext, options?: {
74
+ readonly now?: number;
75
+ readonly record?: StateHandle<SandboxRecord | null>;
76
+ }): Promise<RuntimeSandboxSession>;
77
+ /** A file from the sandbox, as the platform stores it. */
78
+ export interface SandboxFile {
79
+ /** Its real path, with symbolic links resolved. */
80
+ readonly path: string;
81
+ readonly filename: string;
82
+ readonly content: Uint8Array;
83
+ }
84
+ /** The named path is not a file the platform takes from the sandbox; the message says why. */
85
+ export declare class SandboxFileRefused extends TypeError {
86
+ }
87
+ /** The named file is larger than the caller allows. */
88
+ export declare class SandboxFileTooLarge extends RangeError {
89
+ }
90
+ /**
91
+ * Reads a file the model names in the session's sandbox, after the gate. Its real path must stay
92
+ * inside `/workspace`, so a symbolic link cannot hand the platform a file from elsewhere in the
93
+ * machine, and it must be a non-empty regular file of at most `maximumBytes`.
94
+ */
95
+ export declare function sandboxFile(context: SandboxContext, path: string, options: {
96
+ readonly maximumBytes: number;
97
+ readonly now?: number;
98
+ readonly record?: StateHandle<SandboxRecord | null>;
99
+ }): Promise<SandboxFile>;
100
+ /**
101
+ * Deletes the session's sandbox when the session recorded one, such as when its history is
102
+ * cleared. Without a record it does nothing: asking eve for the sandbox would open one.
103
+ */
104
+ export declare function releaseSandbox(context: Pick<SandboxContext, "getSandbox">, record?: StateHandle<SandboxRecord | null>): Promise<boolean>;
@@ -0,0 +1,142 @@
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 { posix } from "node:path";
8
+ import { defineState } from "eve/context";
9
+ const SANDBOX_MARKER_PATH = "/workspace/.segmentstream/sandbox.json";
10
+ const SANDBOX_ATTACHMENTS_DIRECTORY = "/workspace/.eve/attachments";
11
+ const SANDBOX_MAXIMUM_AGE_MS = 864e5;
12
+ const CARRIED_ATTACHMENTS = {
13
+ files: 32,
14
+ bytes: 67108864
15
+ };
16
+ const STAGED_PATH = /^\/workspace\/\.eve\/attachments\/[0-9a-f]{16}\/[\w.-]+$/;
17
+ const sandboxRecord = defineState("segmentstream.sandbox", () => null);
18
+ var SandboxUnavailableError = class extends Error {
19
+ name = "SandboxUnavailableError";
20
+ };
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ function parseSandboxMarker(bytes) {
25
+ if (bytes === null) return;
26
+ let value;
27
+ try {
28
+ value = JSON.parse(Buffer.from(bytes).toString("utf8"));
29
+ } catch {
30
+ return;
31
+ }
32
+ return isRecord(value) && value.version === 1 && typeof value.sessionId === "string" && value.sessionId !== "" && typeof value.openedAt === "string" && !Number.isNaN(Date.parse(value.openedAt)) && typeof value.toolset === "string" && value.toolset !== "" && value.network === "deny-all" ? {
33
+ version: 1,
34
+ sessionId: value.sessionId,
35
+ openedAt: value.openedAt,
36
+ toolset: value.toolset,
37
+ network: "deny-all"
38
+ } : void 0;
39
+ }
40
+ async function initializeSandbox(sandbox, input) {
41
+ const marker = {
42
+ version: 1,
43
+ sessionId: input.sessionId,
44
+ openedAt: (input.now ?? new Date()).toISOString(),
45
+ toolset: input.toolset,
46
+ network: "deny-all"
47
+ };
48
+ await sandbox.writeTextFile({
49
+ path: SANDBOX_MARKER_PATH,
50
+ content: `${JSON.stringify(marker)}\n`
51
+ });
52
+ try {
53
+ (input.record ?? sandboxRecord).update(() => ({ openedAt: marker.openedAt }));
54
+ } catch {}
55
+ return marker;
56
+ }
57
+ async function verifiedMarker(sandbox, sessionId) {
58
+ const marker = parseSandboxMarker(await sandbox.readBinaryFile({ path: SANDBOX_MARKER_PATH }));
59
+ if (marker === void 0 || marker.sessionId !== sessionId) throw new SandboxUnavailableError("The sandbox tools are unavailable: this session's sandbox was not opened by @segmentstream/agent/sandbox. agent/agent/sandbox.ts must re-export it.");
60
+ return marker;
61
+ }
62
+ async function recentAttachments(sandbox, since) {
63
+ const listed = await sandbox.run({ command: `find ${SANDBOX_ATTACHMENTS_DIRECTORY} -type f -newermt @${Math.floor(since / 1e3)} -printf '%T@\\t%s\\t%p\\n' 2>/dev/null | sort -rn` });
64
+ const files = [];
65
+ let bytes = 0;
66
+ for (const line of listed.stdout.split("\n")) {
67
+ const [, size = "", path = ""] = line.split(" ");
68
+ if (!STAGED_PATH.test(path) || files.length >= CARRIED_ATTACHMENTS.files || bytes + Number(size) > CARRIED_ATTACHMENTS.bytes) continue;
69
+ const content = await sandbox.readBinaryFile({ path });
70
+ if (content !== null) {
71
+ files.push({
72
+ path,
73
+ content
74
+ });
75
+ bytes += content.byteLength;
76
+ }
77
+ }
78
+ return files;
79
+ }
80
+ async function sessionSandbox(context, options = {}) {
81
+ const record = options.record ?? sandboxRecord;
82
+ const now = options.now ?? Date.now();
83
+ const turn = context.session.turn.id;
84
+ const sandbox = await context.getSandbox();
85
+ if (record.get()?.checkedTurn === turn) return sandbox;
86
+ const marker = await verifiedMarker(sandbox, context.session.id);
87
+ if (now - Date.parse(marker.openedAt) < 864e5) {
88
+ record.update(() => ({
89
+ openedAt: marker.openedAt,
90
+ checkedTurn: turn
91
+ }));
92
+ return sandbox;
93
+ }
94
+ const carried = await recentAttachments(sandbox, now - SANDBOX_MAXIMUM_AGE_MS);
95
+ await sandbox.delete();
96
+ record.update(() => null);
97
+ const fresh = await context.getSandbox();
98
+ const reopened = await verifiedMarker(fresh, context.session.id);
99
+ for (const file of carried) await fresh.writeBinaryFile(file);
100
+ record.update(() => ({
101
+ openedAt: reopened.openedAt,
102
+ checkedTurn: turn
103
+ }));
104
+ return fresh;
105
+ }
106
+ var SandboxFileRefused = class extends TypeError {};
107
+ var SandboxFileTooLarge = class extends RangeError {};
108
+ function shellQuoted(value) {
109
+ return `'${value.replaceAll("'", `'\\''`)}'`;
110
+ }
111
+ async function sandboxFile(context, path, options) {
112
+ if (!path.startsWith("/workspace/") || /[\0\r\n]/.test(path)) throw new SandboxFileRefused("path must be the absolute path of a file inside /workspace.");
113
+ const sandbox = await sessionSandbox(context, options);
114
+ const described = await sandbox.run({ command: `real="$(realpath -e -- ${shellQuoted(path)})" && printf '%s\\n' "$real" && stat -c '%F|%s' -- "$real"` });
115
+ const [real = "", stat = ""] = described.stdout.split("\n");
116
+ const [kind, size] = stat.split("|");
117
+ if (described.exitCode !== 0 || !real.startsWith("/workspace/") || posix.normalize(real) !== real) throw new SandboxFileRefused(`${path} names no file inside /workspace.`);
118
+ if (kind === "regular empty file") throw new SandboxFileRefused(`${path} is empty.`);
119
+ if (kind !== "regular file") throw new SandboxFileRefused(`${path} is not a regular file.`);
120
+ const tooLarge = () => new SandboxFileTooLarge(`${path} is larger than the ${options.maximumBytes / 1024 / 1024} MB an attachment may be.`);
121
+ if (Number(size) > options.maximumBytes) throw tooLarge();
122
+ const content = await sandbox.readBinaryFile({ path: real });
123
+ const filename = posix.basename(real);
124
+ if (content === null || content.byteLength === 0) throw new SandboxFileRefused(`${path} could not be read.`);
125
+ if (content.byteLength > options.maximumBytes) throw tooLarge();
126
+ if (filename.length > 255 || /[\x00-\x1f\x7f\\]/.test(filename)) throw new SandboxFileRefused(`${filename} cannot be stored as a file name; rename the file.`);
127
+ return {
128
+ path: real,
129
+ filename,
130
+ content
131
+ };
132
+ }
133
+ async function releaseSandbox(context, record = sandboxRecord) {
134
+ if (record.get() === null) return false;
135
+ try {
136
+ await (await context.getSandbox()).delete();
137
+ } finally {
138
+ record.update(() => null);
139
+ }
140
+ return true;
141
+ }
142
+ export { CARRIED_ATTACHMENTS, SANDBOX_ATTACHMENTS_DIRECTORY, SANDBOX_MARKER_PATH, SANDBOX_MAXIMUM_AGE_MS, SandboxFileRefused, SandboxFileTooLarge, SandboxUnavailableError, initializeSandbox, parseSandboxMarker, releaseSandbox, sandboxFile, sandboxRecord, sessionSandbox };
@@ -0,0 +1,38 @@
1
+ import { type SandboxSession } from "eve/sandbox";
2
+ /**
3
+ * `@segmentstream/agent/sandbox`: the isolated sandbox of a workspace agent (`docs/agent.md`,
4
+ * "Sandbox"). eve takes a sandbox only from the agent itself, never from an extension, so the
5
+ * agent's `agent/agent/sandbox.ts` re-exports this module:
6
+ *
7
+ * export { default, environment } from "@segmentstream/agent/sandbox";
8
+ *
9
+ * It replaces eve's default sandbox, which on Vercel opens with an unrestricted network, with a
10
+ * Vercel Sandbox that has no network at all. The model reaches it through the extension's file
11
+ * tools; company data stays behind the ontology tools in the app runtime, so no credential, token
12
+ * or MCP connection ever enters it.
13
+ */
14
+ /**
15
+ * The file toolset, installed once into the environment's snapshot while the network is still
16
+ * open. The Ubuntu packages exist in Ubuntu 26.04, the eve base image's release; a missing name
17
+ * would fail preparation and so the deploy. pdfplumber, which Ubuntu does not ship, is pinned with
18
+ * the two dependencies Ubuntu's are too old for, by version and hash, into the sandbox user's own
19
+ * site-packages; its pdfminer.six 20260107 is Ubuntu's python3-pdfminer.
20
+ */
21
+ export declare const SANDBOX_PREPARE_SCRIPT = "set -euo pipefail\nsudo apt-get update -q\nsudo env DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends \\\n poppler-utils qpdf tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus 7zip unar \\\n imagemagick ghostscript python3-pandas python3-openpyxl python3-pypdf python3-pdfminer\nsudo apt-get clean\nsudo rm -rf /var/lib/apt/lists/*\nrequirements=\"$(mktemp)\"\ncat > \"$requirements\" <<'REQUIREMENTS'\npdfplumber==0.11.10 \\\n --hash=sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580\npillow==12.2.0 \\\n --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \\\n --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \\\n --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \\\n --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601\npypdfium2==5.9.0 \\\n --hash=sha256:7ce3a3dd23ec0adaa079d8be54565ba2aa2f6060e76a4989cd42dabc163d74ee \\\n --hash=sha256:1d902e03dff5efd51d93cd23d3e55bde53802fa6207bcd0e455239518859a069\nREQUIREMENTS\npython3 -m pip install --user --break-system-packages --no-cache-dir --no-deps \\\n --only-binary=:all: --require-hashes --disable-pip-version-check -r \"$requirements\"\nrm -f \"$requirements\"\nmkdir -p /workspace/work\nfor tool in pdftotext pdftoppm qpdf tesseract gs 7z unar unzip jq rg; do\n command -v \"$tool\" > /dev/null\ndone\nlanguages=\"$(tesseract --list-langs)\"\ngrep -qx eng <<< \"$languages\"\ngrep -qx rus <<< \"$languages\"\npython3 -c 'import openpyxl, pandas, pdfminer, pdfplumber, pypdf, pypdfium2'\n";
22
+ /**
23
+ * Identifies the toolset. eve keys a prepared snapshot on the authored `sandbox.ts` and the
24
+ * environment's options, not on a `prepare` imported from a package, so the version travels as a
25
+ * sandbox tag: a changed toolset is a new snapshot.
26
+ */
27
+ export declare const SANDBOX_TOOLSET_VERSION: string;
28
+ /** Each live sandbox's compute; Vercel pairs every vCPU with 2 GB of memory. */
29
+ export declare const SANDBOX_VCPUS = 2;
30
+ export declare function prepareSandbox(sandbox: SandboxSession): Promise<void>;
31
+ /**
32
+ * The environment `eve build` prepares. It carries no network policy: one applied here would hold
33
+ * during preparation too, before the toolset is installed.
34
+ */
35
+ export declare const environment: import("eve/sandbox").SandboxEnvironment<import("eve/sandbox/vercel").VercelSandboxRuntimeOptions, import("eve/sandbox/vercel").VercelSandboxSession>;
36
+ /** Opens the session's sandbox without network access and marks it as the SDK's. */
37
+ declare const _default: import("eve/sandbox").SandboxSelector;
38
+ export default _default;
@@ -0,0 +1,64 @@
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 { initializeSandbox } from "./sandbox-session.mjs";
8
+ import { createHash } from "node:crypto";
9
+ import { defineSandbox } from "eve/sandbox";
10
+ import { VercelSandbox } from "eve/sandbox/vercel";
11
+ const SANDBOX_PREPARE_SCRIPT = `set -euo pipefail
12
+ sudo apt-get update -q
13
+ sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y -q --no-install-recommends \\
14
+ poppler-utils qpdf tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus 7zip unar \\
15
+ imagemagick ghostscript python3-pandas python3-openpyxl python3-pypdf python3-pdfminer
16
+ sudo apt-get clean
17
+ sudo rm -rf /var/lib/apt/lists/*
18
+ requirements="$(mktemp)"
19
+ cat > "$requirements" <<'REQUIREMENTS'
20
+ pdfplumber==0.11.10 \\
21
+ --hash=sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580
22
+ pillow==12.2.0 \\
23
+ --hash=sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286 \\
24
+ --hash=sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be \\
25
+ --hash=sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f \\
26
+ --hash=sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601
27
+ pypdfium2==5.9.0 \\
28
+ --hash=sha256:7ce3a3dd23ec0adaa079d8be54565ba2aa2f6060e76a4989cd42dabc163d74ee \\
29
+ --hash=sha256:1d902e03dff5efd51d93cd23d3e55bde53802fa6207bcd0e455239518859a069
30
+ REQUIREMENTS
31
+ python3 -m pip install --user --break-system-packages --no-cache-dir --no-deps \\
32
+ --only-binary=:all: --require-hashes --disable-pip-version-check -r "$requirements"
33
+ rm -f "$requirements"
34
+ mkdir -p /workspace/work
35
+ for tool in pdftotext pdftoppm qpdf tesseract gs 7z unar unzip jq rg; do
36
+ command -v "$tool" > /dev/null
37
+ done
38
+ languages="$(tesseract --list-langs)"
39
+ grep -qx eng <<< "$languages"
40
+ grep -qx rus <<< "$languages"
41
+ python3 -c 'import openpyxl, pandas, pdfminer, pdfplumber, pypdf, pypdfium2'
42
+ `;
43
+ const SANDBOX_TOOLSET_VERSION = createHash("sha256").update(SANDBOX_PREPARE_SCRIPT).digest("hex").slice(0, 16);
44
+ const SANDBOX_VCPUS = 2;
45
+ async function prepareSandbox(sandbox) {
46
+ const result = await sandbox.run({ command: SANDBOX_PREPARE_SCRIPT });
47
+ if (result.exitCode !== 0) throw new Error(`The SegmentStream sandbox toolset failed to install (exit ${result.exitCode}): ` + result.stderr.slice(-4e3));
48
+ }
49
+ const environment = VercelSandbox.environment({
50
+ prepare: prepareSandbox,
51
+ tags: { segmentstreamToolset: SANDBOX_TOOLSET_VERSION }
52
+ });
53
+ var sandbox_default = defineSandbox(async ({ session }) => {
54
+ const sandbox = await environment.open({
55
+ networkPolicy: "deny-all",
56
+ resources: { vcpus: 2 }
57
+ });
58
+ await initializeSandbox(sandbox, {
59
+ sessionId: session.id,
60
+ toolset: SANDBOX_TOOLSET_VERSION
61
+ });
62
+ return sandbox;
63
+ });
64
+ export { SANDBOX_PREPARE_SCRIPT, SANDBOX_TOOLSET_VERSION, SANDBOX_VCPUS, sandbox_default as default, environment, prepareSandbox };
@@ -20,7 +20,7 @@ import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
20
20
  import { dispatchStructuredMessage, structuredMessage } from "./structured.mjs";
21
21
  import { scopedAddress, sessionPolicy } from "./sessions.mjs";
22
22
  import { handleWorkspaceCallback, isWorkspaceCallback } from "./callbacks.mjs";
23
- import { t as agentBuild } from "../../../_chunks/build-metadata-CIFPiRho.mjs";
23
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DR4gACLZ.mjs";
24
24
  import { attachmentLifecycle } from "./lifecycle.mjs";
25
25
  import { observedMessages } from "./observed.mjs";
26
26
  import { expireDeferredCards, handleDeferredCallback, isDeferredCallback } from "./deferred-confirmations.mjs";
@@ -29,7 +29,7 @@ import { gatewayAttachmentDescriber } from "./attachment-description.mjs";
29
29
  import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs";
30
30
  import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
31
31
  import { agentIdentities } from "../identities.mjs";
32
- import { replyDelivery } from "./silent.mjs";
32
+ import { interimTexts, replyDelivery } from "./silent.mjs";
33
33
  import { handleConfirmationCallback, telegramConfirmationEvents } from "./confirmations.mjs";
34
34
  import { captureRouteDecider, captureTelegramFetch, conversationIdentities, recordGateDecision, telegramConversationCapture } from "./conversations.mjs";
35
35
  import { runtimeTokenVerifier } from "../runtime-identity.mjs";
@@ -88,6 +88,7 @@ function segmentstreamTelegram(options) {
88
88
  });
89
89
  const bot = botIdentityCache();
90
90
  const proxies = new Map();
91
+ const interimDelivered = interimTexts();
91
92
  let identities;
92
93
  let verify = options.verify;
93
94
  function settings() {
@@ -372,7 +373,10 @@ function segmentstreamTelegram(options) {
372
373
  }
373
374
  },
374
375
  events: {
375
- ...telegramFailureEvents,
376
+ ...telegramFailureEvents({
377
+ archive,
378
+ identities: pairing
379
+ }),
376
380
  ...telegramConfirmationEvents(archive, observed, {
377
381
  environment,
378
382
  token,
@@ -415,7 +419,10 @@ function segmentstreamTelegram(options) {
415
419
  await channel.telegram.post(failureText(reference));
416
420
  return;
417
421
  }
418
- await replyDelivery(observed, () => telegramSendScope(environment, settings().connection))(event, channel, context);
422
+ await replyDelivery(observed, () => telegramSendScope(environment, settings().connection), {
423
+ enabled: () => settings().interimMessages === true,
424
+ delivered: interimDelivered
425
+ })(event, channel, context);
419
426
  }
420
427
  }
421
428
  });
@@ -28,6 +28,22 @@ export declare function recordInboundContent(content: {
28
28
  readonly media?: string;
29
29
  readonly decoded?: ReadonlyArray<DecodedAttachment>;
30
30
  }): Promise<void>;
31
+ /**
32
+ * Records eve's unrecoverable session failure from the channel. The channel receives it even when
33
+ * eve publishes it after its workflow loop throws, which runs no hooks. Never throws, so the
34
+ * member's notice never depends on the archive.
35
+ */
36
+ export declare function recordSessionFailure(input: {
37
+ readonly archive: Conversations;
38
+ readonly identities: () => AgentIdentities;
39
+ readonly chatId: string | null;
40
+ readonly threadId: string | null;
41
+ readonly failure: {
42
+ readonly code: string;
43
+ readonly message: string;
44
+ readonly sessionId: string;
45
+ };
46
+ }): Promise<void>;
31
47
  /** Captures message sends and button/status edits, including pairing and proactive replies. */
32
48
  export declare function captureTelegramFetch(fetchImpl: typeof fetch, archive: Conversations, identities: () => AgentIdentities): typeof fetch;
33
49
  /** Reuses the request's verified lookup; it never survives into another inbound request. */
@@ -5,8 +5,9 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { n as currentInboundRequest, p as reportTelegramError, s as currentThreadId } from "../../../_chunks/inbound-request-RIRNBxUC.mjs";
8
+ import { n as contentText, t as CONTENT_CAPS } from "../../../_chunks/content-CvFsrmSD.mjs";
8
9
  import { currentConfirmationPrompt, observedChatKey } from "../group-routing/observed.mjs";
9
- import { conversationEvent } from "../conversations.mjs";
10
+ import { conversationEvent, sessionFailureEventId } from "../conversations.mjs";
10
11
  import { archivedRoutingMessages } from "../group-routing/routing-context.mjs";
11
12
  import { contentShape, messageContent } from "./content.mjs";
12
13
  import { messageMetadata } from "./observed.mjs";
@@ -517,6 +518,62 @@ async function recordInboundContent(content) {
517
518
  await capture.archive.append(event);
518
519
  capture.event = event;
519
520
  }
521
+ async function outboundAudience(chatId, identities) {
522
+ const inbound = currentInboundRequest();
523
+ const capture = inbound === void 0 ? void 0 : captures.get(inbound);
524
+ let participantIds = capture?.event.participantIds ?? [];
525
+ const chatType = capture?.event.chatType ?? (chatId.startsWith("-") ? "group" : "private");
526
+ if (chatType === "private" && participantIds.length === 0 && capture === void 0) try {
527
+ const lookup = await identities().lookup({
528
+ provider: "telegram",
529
+ externalId: chatId
530
+ });
531
+ if (lookup.status === "paired") participantIds = [lookup.member.id];
532
+ } catch (error) {
533
+ console.error("[segmentstream] outbound participant lookup failed", { error });
534
+ }
535
+ return {
536
+ capture,
537
+ chatType,
538
+ participantIds
539
+ };
540
+ }
541
+ async function recordSessionFailure(input) {
542
+ const { failure } = input;
543
+ const sessionId = failure.sessionId === "" ? null : failure.sessionId;
544
+ try {
545
+ const errorText = contentText(failure.message, CONTENT_CAPS.error);
546
+ if (input.chatId === null) {
547
+ console.error("[segmentstream] agent log dropped unaddressed failure", {
548
+ sessionId,
549
+ eventType: "session.failed",
550
+ code: failure.code,
551
+ message: errorText
552
+ });
553
+ return;
554
+ }
555
+ const { chatType, participantIds } = await outboundAudience(input.chatId, input.identities);
556
+ await input.archive.append(conversationEvent({
557
+ eventId: sessionId === null ? `eve:session.failed:${randomUUID()}` : sessionFailureEventId(sessionId),
558
+ provider: "telegram",
559
+ chatId: input.chatId,
560
+ chatType,
561
+ threadId: input.threadId,
562
+ participantIds,
563
+ sessionId,
564
+ kind: "decision",
565
+ decision: "silent",
566
+ reason: "session.failed",
567
+ status: "failed",
568
+ errorText
569
+ }));
570
+ } catch (error) {
571
+ console.error("[segmentstream] recording session failure failed", {
572
+ sessionId,
573
+ error
574
+ });
575
+ }
576
+ }
520
577
  function captureTelegramFetch(fetchImpl, archive, identities) {
521
578
  return async (input, init) => {
522
579
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -525,19 +582,7 @@ function captureTelegramFetch(fetchImpl, archive, identities) {
525
582
  const body = record(JSON.parse(init.body));
526
583
  const chatId = id(body.chat_id);
527
584
  if (chatId === null) return fetchImpl(input, init);
528
- const inbound = currentInboundRequest();
529
- const capture = inbound === void 0 ? void 0 : captures.get(inbound);
530
- let participantIds = capture?.event.participantIds ?? [];
531
- const chatType = capture?.event.chatType ?? (chatId.startsWith("-") ? "group" : "private");
532
- if (chatType === "private" && participantIds.length === 0 && capture === void 0) try {
533
- const lookup = await identities().lookup({
534
- provider: "telegram",
535
- externalId: chatId
536
- });
537
- if (lookup.status === "paired") participantIds = [lookup.member.id];
538
- } catch (error) {
539
- console.error("[segmentstream] outbound participant lookup failed", { error });
540
- }
585
+ const { capture, chatType, participantIds } = await outboundAudience(chatId, identities);
541
586
  const event = conversationEvent({
542
587
  eventId: `telegram:${edit ? "edit" : "send"}:${randomUUID()}`,
543
588
  provider: "telegram",
@@ -681,4 +726,4 @@ function telegramConversationCapture(input) {
681
726
  }
682
727
  };
683
728
  }
684
- export { REVISION_CLAIM_TTL_MS, captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordInboundContent, recordRouteDecision, telegramConversationCapture };
729
+ export { REVISION_CLAIM_TTL_MS, captureRouteDecider, captureTelegramFetch, captureTelegramWebhook, conversationIdentities, recordGateDecision, recordInboundContent, recordRouteDecision, recordSessionFailure, telegramConversationCapture };
@@ -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 { t as agentBuild } from "../../../_chunks/build-metadata-CIFPiRho.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DR4gACLZ.mjs";
8
8
  import { runtimeRequestFailure } from "./runtime-request.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
@@ -9,7 +9,7 @@ import { object } from "../confirmation-description.mjs";
9
9
  import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-RIRNBxUC.mjs";
10
10
  import { needsConfirmation } from "../confirmations.mjs";
11
11
  import { OntologyActionNotPermitted, actionToolName, ontologyTools } from "../ontology-tools.mjs";
12
- import { t as agentBuild } from "../../../_chunks/build-metadata-CIFPiRho.mjs";
12
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DR4gACLZ.mjs";
13
13
  var AttachmentLifecycleNotPermitted = class extends Error {
14
14
  code = "attachment_lifecycle_not_permitted";
15
15
  };
@@ -4,10 +4,32 @@ export declare function isSilentReply(text: string): boolean;
4
4
  /** Use the same classification for delivery and its archive decision. Never strip real prose. */
5
5
  export declare function completionKind(message: string | null | undefined, finishReason?: string): "reply" | "tool_calls" | "empty" | "whitespace" | "silent_marker";
6
6
  type MessageCompleted = NonNullable<TelegramChannelEvents["message.completed"]>;
7
+ /** The texts one runtime delivered as interim messages, per session turn. */
8
+ export interface InterimTexts {
9
+ has(turn: string, text: string): boolean;
10
+ add(turn: string, text: string): void;
11
+ }
12
+ /** How many recent turns {@link interimTexts} remembers. */
13
+ export declare const INTERIM_TURNS = 256;
14
+ /** Remembers the interim texts of the most recent turns, the oldest turn forgotten first. */
15
+ export declare function interimTexts(limit?: number): InterimTexts;
16
+ /**
17
+ * `channels.telegram.interimMessages`: the text the model writes in a step that ends in tool
18
+ * calls, such as a word on what a long job is doing, reaches the chat as its own message.
19
+ */
20
+ export interface InterimMessages {
21
+ readonly enabled: () => boolean;
22
+ /** What this runtime delivered, so neither an interim text nor a final reply goes out twice. */
23
+ readonly delivered: InterimTexts;
24
+ }
7
25
  /**
8
26
  * The channel's final reply: formatted and split before Eve's plain-text splitter, with one
9
- * queue slot for the whole reply. Tool-call steps and explicit silence markers have no delivery.
27
+ * queue slot for the whole reply. Explicit silence markers have no delivery. Tool-call steps have
28
+ * none either unless `interim` is enabled: then their text is delivered exactly like a reply, but
29
+ * not remembered for group routing, which counts only final replies, as its recovery from the
30
+ * session stream does. A text already delivered as an interim message in the same turn, whether
31
+ * a repeat or the final reply, is not delivered again.
10
32
  */
11
- export declare function replyDelivery(observed?: ObservedMessages, scope?: () => string): MessageCompleted;
33
+ export declare function replyDelivery(observed?: ObservedMessages, scope?: () => string, interim?: InterimMessages): MessageCompleted;
12
34
  export declare const deliverUnlessSilent: MessageCompleted;
13
35
  export {};
@@ -20,9 +20,31 @@ function completionKind(message, finishReason) {
20
20
  if (message.trim() === "") return "whitespace";
21
21
  return isSilentReply(message) ? "silent_marker" : "reply";
22
22
  }
23
- function replyDelivery(observed, scope = () => "telegram") {
23
+ const INTERIM_TURNS = 256;
24
+ function interimTexts(limit = 256) {
25
+ const turns = new Map();
26
+ return {
27
+ has: (turn, text) => turns.get(turn)?.has(text) ?? false,
28
+ add(turn, text) {
29
+ const texts = turns.get(turn) ?? new Set();
30
+ turns.delete(turn);
31
+ turns.set(turn, texts.add(text));
32
+ for (const oldest of turns.keys()) {
33
+ if (turns.size <= limit) break;
34
+ turns.delete(oldest);
35
+ }
36
+ }
37
+ };
38
+ }
39
+ function replyDelivery(observed, scope = () => "telegram", interim) {
24
40
  return async (event, channel, context) => {
25
- if (completionKind(event.message, event.finishReason) !== "reply") return;
41
+ const kind = completionKind(event.message, event.finishReason);
42
+ const interimOn = interim?.enabled() === true;
43
+ const isInterim = kind === "tool_calls" && interimOn && completionKind(event.message) === "reply";
44
+ if (kind !== "reply" && !isInterim) return;
45
+ const turn = interimOn && typeof event.turnId === "string" && event.turnId !== "" ? JSON.stringify([context.session?.id ?? "", event.turnId]) : void 0;
46
+ const key = event.message.trim();
47
+ if (turn !== void 0 && interim.delivered.has(turn, key)) return;
26
48
  const message = customerMessage(event.message, confirmationLocale(context.session?.auth.current ?? null, currentLocale()));
27
49
  const principal = context.session?.auth.current;
28
50
  const replyTo = principal?.attributes.replyToMessageId;
@@ -42,12 +64,16 @@ function replyDelivery(observed, scope = () => "telegram") {
42
64
  messageIds.push(Number(result.id));
43
65
  }
44
66
  } catch (error) {
45
- const reference = reportTelegramError("telegram_reply_delivery_failed", error, { messageIds });
67
+ const reference = reportTelegramError(isInterim ? "telegram_interim_delivery_failed" : "telegram_reply_delivery_failed", error, { messageIds });
46
68
  throw new TelegramDeliveryError(reference, error instanceof TelegramDeliveryError ? error.delivery : "unknown", messageIds);
47
69
  }
48
70
  return messageIds;
49
71
  });
50
72
  const text = message.text;
73
+ if (isInterim) {
74
+ if (turn !== void 0) interim.delivered.add(turn, key);
75
+ return;
76
+ }
51
77
  if (observed === void 0 || channel.telegram.chatType === "private") return;
52
78
  const sender = principal?.authenticator === "segmentstream-telegram" ? principal.subject : void 0;
53
79
  const username = principal?.attributes.username;
@@ -71,4 +97,4 @@ function replyDelivery(observed, scope = () => "telegram") {
71
97
  };
72
98
  }
73
99
  const deliverUnlessSilent = replyDelivery();
74
- export { completionKind, deliverUnlessSilent, isSilentReply, replyDelivery };
100
+ export { INTERIM_TURNS, completionKind, deliverUnlessSilent, interimTexts, isSilentReply, replyDelivery };
@@ -1,2 +1,8 @@
1
1
  import type { TelegramChannelEvents } from "eve/channels/telegram";
2
- export declare const telegramFailureEvents: Pick<TelegramChannelEvents, "turn.failed" | "turn.cancelled" | "session.failed" | "authorization.completed" | "authorization.required">;
2
+ import type { Conversations } from "../conversations.js";
3
+ import type { AgentIdentities } from "../identities.js";
4
+ /** Failure notices for members; an unrecoverable session failure is also recorded to the archive. */
5
+ export declare function telegramFailureEvents(input: {
6
+ readonly archive: Conversations;
7
+ readonly identities: () => AgentIdentities;
8
+ }): Pick<TelegramChannelEvents, "turn.failed" | "turn.cancelled" | "session.failed" | "authorization.completed" | "authorization.required">;