@segmentstream/agent 0.17.6 → 0.17.7

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.
@@ -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
- var version = "0.17.6";
7
+ var version = "0.17.7";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -4,5 +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 { t as agentBuild } from "../../_chunks/build-metadata-BkfakQLL.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-DNcZwjhU.mjs";
8
8
  export { agentBuild };
@@ -0,0 +1,3 @@
1
+ export declare const transientFileStatus: (status: number) => boolean;
2
+ /** Only for reads that issue a file grant; never wrap a delivery or a mutation. */
3
+ export declare function fetchFileGrant(send: () => Promise<Response>): Promise<Response>;
@@ -0,0 +1,25 @@
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
+ const transientFileStatus = (status) => [
8
+ 429,
9
+ 502,
10
+ 503,
11
+ 504
12
+ ].includes(status);
13
+ async function fetchFileGrant(send) {
14
+ for (let attempt = 0;; attempt++) {
15
+ const response = await send();
16
+ if (!transientFileStatus(response.status) || attempt === 2) return response;
17
+ const retryAfter = response.headers.get("retry-after");
18
+ const seconds = Number(retryAfter);
19
+ const delay = retryAfter === null ? 500 * 2 ** attempt : Number.isFinite(seconds) ? Math.max(0, seconds * 1e3) : Date.parse(retryAfter) - Date.now();
20
+ if (!Number.isFinite(delay) || delay < 0 || delay > 5e3) return response;
21
+ await response.body?.cancel();
22
+ await new Promise((resolve) => setTimeout(resolve, delay));
23
+ }
24
+ }
25
+ export { fetchFileGrant, transientFileStatus };
@@ -11,6 +11,13 @@ export declare class OntologyToolRejected extends Error {
11
11
  export declare class OntologyActionNotPermitted extends Error {
12
12
  readonly code = "ontology_action_not_permitted";
13
13
  }
14
+ /** A known read-only file request can be repeated; action outcomes keep their existing fence. */
15
+ export declare class OntologyFileUnavailable extends Error {
16
+ readonly code: string;
17
+ readonly requestId: string | undefined;
18
+ readonly retryable = true;
19
+ constructor(code: string, requestId: string | undefined, options?: ErrorOptions);
20
+ }
14
21
  /** A fresh, permission-filtered catalog. Listing tools cannot run an action's prepare step. */
15
22
  export declare function ontologyTools(options?: {
16
23
  readonly principal?: SessionAuthContext | null;
@@ -17,6 +17,16 @@ var OntologyToolRejected = class extends Error {
17
17
  var OntologyActionNotPermitted = class extends Error {
18
18
  code = "ontology_action_not_permitted";
19
19
  };
20
+ var OntologyFileUnavailable = class extends Error {
21
+ code;
22
+ requestId;
23
+ retryable = true;
24
+ constructor(code, requestId, options) {
25
+ super(`${code}${requestId === void 0 ? "" : ` (${requestId})`}`, options);
26
+ this.code = code;
27
+ this.requestId = requestId;
28
+ }
29
+ };
20
30
  function ontologyTools(options = {}) {
21
31
  const runtimeSources = options.runtimeSources ?? new Map();
22
32
  async function rpc(memberId, method, params, signal) {
@@ -61,6 +71,8 @@ function ontologyTools(options = {}) {
61
71
  const result = object(body.result);
62
72
  if (method === "tools/list" && Array.isArray(result.tools)) runtimeSources.set(memberId, result.tools.map(object).some((tool) => object(tool._meta)[RUNTIME_SOURCE_PROTOCOL] === 1));
63
73
  if (result.isError === true) {
74
+ const failure = object(result.structuredContent);
75
+ if (method === "tools/call" && params.name === "files_download" && failure.retryable === true && ["ontology_updating", "storage_unavailable"].includes(String(failure.error))) throw new OntologyFileUnavailable(String(failure.error), typeof failure.request_id === "string" ? failure.request_id : void 0, { cause: result });
64
76
  const message = (Array.isArray(result.content) ? result.content : []).map(object).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
65
77
  if ([
66
78
  "action_rejected",
@@ -87,6 +99,7 @@ function ontologyTools(options = {}) {
87
99
  reference,
88
100
  error: diagnostic
89
101
  });
102
+ if (error instanceof OntologyFileUnavailable) throw new OntologyFileUnavailable(error.code, error.requestId, { cause: diagnostic });
90
103
  if (error instanceof OntologyToolRejected) throw new OntologyToolRejected(String(object(diagnostic).message), { cause: diagnostic });
91
104
  throw new Error(`The ontology request failed (${reference}).`, { cause: diagnostic });
92
105
  }
@@ -246,4 +259,4 @@ function promotionTarget(input) {
246
259
  }
247
260
  };
248
261
  }
249
- export { OntologyActionNotPermitted, OntologyToolRejected, actionToolName, ontologyToolLabels, ontologyTools, promotionTarget };
262
+ export { OntologyActionNotPermitted, OntologyFileUnavailable, OntologyToolRejected, actionToolName, ontologyToolLabels, ontologyTools, promotionTarget };
@@ -7,18 +7,27 @@ __eveCreateRequire(import.meta.url);
7
7
  import { i as memberIdOf } from "../../_chunks/principal-B6u41vIf.mjs";
8
8
  import { redactDiagnostic } from "./diagnostics.mjs";
9
9
  import { object } from "./confirmation-description.mjs";
10
- import { ontologyTools } from "./ontology-tools.mjs";
10
+ import { OntologyFileUnavailable, ontologyTools } from "./ontology-tools.mjs";
11
11
  import { createHash } from "node:crypto";
12
12
  async function readStoredFile(input, principal, options = {}) {
13
13
  const memberId = memberIdOf(principal);
14
14
  if (memberId === void 0) throw new Error("A current member is required to read a stored file.");
15
- const raw = await ontologyTools({
15
+ const tools = ontologyTools({
16
16
  ...options,
17
17
  principal
18
- }).rpc(memberId, "tools/call", {
19
- name: "files_download",
20
- arguments: input
21
18
  });
19
+ const readGrant = async () => {
20
+ for (let attempt = 0;; attempt++) try {
21
+ return await tools.rpc(memberId, "tools/call", {
22
+ name: "files_download",
23
+ arguments: input
24
+ });
25
+ } catch (error) {
26
+ if (!(error instanceof OntologyFileUnavailable) || attempt === 2) throw error;
27
+ await new Promise((resolve) => setTimeout(resolve, error.code === "ontology_updating" ? 5e3 : 500 * 2 ** attempt));
28
+ }
29
+ };
30
+ const raw = await readGrant();
22
31
  const result = object(raw.structuredContent), file = object(result.file), grant = object(result.download);
23
32
  const headers = object(grant.headers);
24
33
  const size = file.size;
@@ -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-BkfakQLL.mjs";
23
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DNcZwjhU.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";
@@ -20,6 +20,9 @@ export interface SentFile {
20
20
  readonly error?: string;
21
21
  readonly detail?: string;
22
22
  readonly reference?: string;
23
+ readonly status?: number;
24
+ readonly retryable?: boolean;
25
+ readonly requestId?: string;
23
26
  }
24
27
  /** Bare ids are document records; explicit references select other entities and file properties. */
25
28
  export declare function sendAttachmentsInput(value: Record<string, unknown>): SendAttachmentsInput;
@@ -16,6 +16,7 @@ import { ACT_AS_HEADER } from "../ontology-connection.mjs";
16
16
  import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
17
17
  import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
18
18
  import { attachmentOwner } from "../attachment-tools.mjs";
19
+ import { fetchFileGrant, transientFileStatus } from "../file-grant.mjs";
19
20
  const MAXIMUM_PHOTO_BYTES = 10485760;
20
21
  const MAXIMUM_DOCUMENT_BYTES = 52428800;
21
22
  const PHOTOS = new Set([
@@ -133,12 +134,14 @@ async function sendAttachmentsInOrder(options) {
133
134
  const uploads = [];
134
135
  for (const [index, reference] of options.input.documentIds.entries()) {
135
136
  let filename = "";
137
+ let requestId;
136
138
  try {
137
- const response = await fetchImpl(`${origin}/api/ontology/agent/files/${[
139
+ const path = [
138
140
  reference.entity,
139
141
  reference.id,
140
142
  ...reference.property === void 0 ? [] : [reference.property]
141
- ].map(encodeURIComponent).join("/")}`, {
143
+ ].map(encodeURIComponent).join("/");
144
+ const response = await fetchFileGrant(async () => fetchImpl(`${origin}/api/ontology/agent/files/${path}`, {
142
145
  headers: {
143
146
  authorization: `Bearer ${await token()}`,
144
147
  [ACT_AS_HEADER]: owner.memberId,
@@ -147,10 +150,18 @@ async function sendAttachmentsInOrder(options) {
147
150
  },
148
151
  redirect: "error",
149
152
  signal: AbortSignal.timeout(3e4)
150
- });
153
+ }));
151
154
  if (!response.ok) {
152
- const code = response.status === 404 ? "not_found" : response.status === 403 ? "forbidden" : response.status === 422 ? "unsupported" : "unavailable";
153
- throw refused(code, `The ontology refused the file (${code}).`);
155
+ const body = await response.json().catch(() => void 0);
156
+ const upstream = isRecord(body) ? body : {};
157
+ const ref = upstream.request_id ?? upstream.requestId;
158
+ requestId = typeof ref === "string" ? ref : void 0;
159
+ const code = response.status === 404 ? "not_found" : response.status === 403 ? "forbidden" : response.status === 422 ? "unsupported" : response.status === 503 && upstream.error === "ontology_updating" ? "ontology_updating" : transientFileStatus(response.status) ? "retryable" : "unavailable";
160
+ throw new AttachmentError({
161
+ code,
162
+ message: code,
163
+ status: response.status
164
+ });
154
165
  }
155
166
  const answer = await response.json();
156
167
  const file = isRecord(answer) ? answer.file : void 0;
@@ -191,7 +202,12 @@ async function sendAttachmentsInOrder(options) {
191
202
  filename,
192
203
  error: error instanceof AttachmentError ? error.code : "unavailable",
193
204
  detail: failureText(errorId, "attachment", locale),
194
- reference: errorId
205
+ reference: errorId,
206
+ ...error instanceof AttachmentError && error.status !== void 0 ? {
207
+ status: error.status,
208
+ retryable: transientFileStatus(error.status)
209
+ } : {},
210
+ ...requestId === void 0 ? {} : { requestId }
195
211
  };
196
212
  }
197
213
  }
@@ -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-BkfakQLL.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DNcZwjhU.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-DxRm5LC8.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-BkfakQLL.mjs";
12
+ import { t as agentBuild } from "../../../_chunks/build-metadata-DNcZwjhU.mjs";
13
13
  var AttachmentLifecycleNotPermitted = class extends Error {
14
14
  code = "attachment_lifecycle_not_permitted";
15
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
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",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@chat-adapter/telegram": "4.41.0",
69
- "@segmentstream-cloud/ontology": "0.54.2",
69
+ "@segmentstream-cloud/ontology": "0.54.3",
70
70
  "@vercel/oidc": "^3.8.4",
71
71
  "ai": "^7.0.128",
72
72
  "csv-parse": "^7.0.3",