From 9e683c0d920cc3f3720821dafd2bc1f4bbf8cf32 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 21:00:17 -0500 Subject: [PATCH 1/7] Add support for listing and deleting streams --- CHANGELOG.md | 135 +++++++++++++++++++++++ package-lock.json | 218 +------------------------------------ package.json | 2 +- src/apis/stream.ts | 156 +++++++++++++++++++++++++- src/types.ts | 96 ++++++++++++++++ test/stream.test.ts | 260 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 648 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 225516a..9deb899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,140 @@ # @agentuity/sdk Changelog +## 0.0.156 + +### Patch Changes + +- You can now list and search through your streams with flexible filtering and pagination options. + + ```typescript + import { streams } from "@agentuity/sdk"; + + // List all streams + const result = await streams.list(); + console.log(`Found ${result.total} streams`); + result.streams.forEach((stream) => { + console.log(`${stream.name}: ${stream.url} (${stream.sizeBytes} bytes)`); + }); + + // Filter by name + const namedStreams = await streams.list({ + name: "my-report", + }); + + // Filter by metadata + const customerStreams = await streams.list({ + metadata: { customerId: "customer-123", type: "invoice" }, + }); + + // Paginate results + const page1 = await streams.list({ limit: 10, offset: 0 }); + const page2 = await streams.list({ limit: 10, offset: 10 }); + + // Combine filters + const filtered = await streams.list({ + name: "analytics", + metadata: { department: "sales" }, + limit: 50, + offset: 0, + }); + ``` + + **Response Format:** + + - `success`: Boolean indicating if the request succeeded + - `streams`: Array of stream objects with `id`, `name`, `metadata`, `url`, and `sizeBytes` + - `total`: Total count of streams matching your filters (useful for pagination) + - `message`: Optional error message if request failed + + **Pagination:** + + - Default limit: 100 streams + - Maximum limit: 1000 streams + - Use `offset` to skip streams for pagination + + You can now delete streams by their ID when they're no longer needed. + + ```typescript + import { streams } from "@agentuity/sdk"; + + // Delete a stream + await streams.delete("stream-id-123"); + + // Handle deletion with error checking + try { + await streams.delete(streamId); + console.log("Stream deleted successfully"); + } catch (error) { + if (error.message.includes("not found")) { + console.log("Stream does not exist"); + } else { + console.error("Failed to delete stream:", error); + } + } + ``` + + **Notes:** + + - Returns nothing on success + - Throws error if stream ID is invalid or empty + - Throws "Stream not found" error if stream doesn't exist + - Stream IDs must be non-empty strings + + ```typescript + import { streams } from "@agentuity/sdk"; + + // Create a stream + const stream = await streams.create("user-export", { + metadata: { userId: "user-123", format: "csv" }, + }); + + await stream.write("Name,Email\n"); + await stream.write("John,john@example.com\n"); + await stream.close(); + + console.log(`Stream created: ${stream.url}`); + + // List streams by metadata + const userStreams = await streams.list({ + metadata: { userId: "user-123" }, + }); + + console.log(`Found ${userStreams.total} streams for user`); + + // Clean up old streams + for (const oldStream of userStreams.streams) { + await streams.delete(oldStream.id); + console.log(`Deleted: ${oldStream.name}`); + } + ``` + + If you're currently managing streams, you can now: + + - **Search and organize**: Use the list API to find streams by name or metadata instead of tracking IDs manually + - **Paginate large lists**: Process streams in batches using limit/offset + - **Clean up**: Delete streams that are no longer needed to manage storage + - **Audit**: Use metadata filters to find streams by customer, project, or any custom tags + + **Parameters:** + + - `name` (optional): Filter streams by name + - `metadata` (optional): Object with key-value pairs to filter by metadata + - `limit` (optional): Maximum streams to return (1-1000, default: 100) + - `offset` (optional): Number of streams to skip for pagination + + **Returns:** `Promise` + + **Parameters:** + + - `id` (required): The stream ID to delete + + **Returns:** `Promise` + + **Throws:** + + - Error if ID is invalid, empty, or only whitespace + - "Stream not found" if stream doesn't exist + ## 0.0.155 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 952f05c..812e80a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentuity/sdk", - "version": "0.0.155", + "version": "0.0.156", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentuity/sdk", - "version": "0.0.155", + "version": "0.0.156", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -5679,35 +5679,6 @@ "dev": true, "license": "MIT" }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5784,20 +5755,6 @@ "node": "*" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/botbuilder": { "version": "4.23.3", "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.3.tgz", @@ -5983,20 +5940,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -6151,32 +6094,6 @@ "node": ">=8" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -6815,20 +6732,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fix-dts-default-cjs-exports": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", @@ -7050,20 +6953,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/google-auth-library": { "version": "9.15.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", @@ -7348,20 +7237,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7393,17 +7268,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -7413,20 +7277,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -7446,17 +7296,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8014,17 +7853,6 @@ "node": ">=6.0.0" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nunjucks": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", @@ -8472,34 +8300,6 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9131,20 +8931,6 @@ "tlds": "bin.js" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index 3cb7d91..53477bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentuity/sdk", - "version": "0.0.155", + "version": "0.0.156", "description": "The Agentuity SDK for NodeJS and Bun", "license": "Apache-2.0", "public": true, diff --git a/src/apis/stream.ts b/src/apis/stream.ts index 2e819cf..2a204d1 100644 --- a/src/apis/stream.ts +++ b/src/apis/stream.ts @@ -1,6 +1,12 @@ -import { getBaseUrlForService, POST, getFetch } from './api'; +import { getBaseUrlForService, POST, DELETE, getFetch } from './api'; import { getSDKVersion, getTracer, recordException } from '../router/router'; -import type { CreateStreamProps, Stream, StreamAPI } from '../types'; +import type { + CreateStreamProps, + Stream, + StreamAPI, + ListStreamsParams, + ListStreamsResponse, +} from '../types'; import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { safeStringify } from '../utils/stringify'; import { ReadableStream } from 'node:stream/web'; @@ -420,4 +426,150 @@ export default class StreamAPIImpl implements StreamAPI { span.end(); } } + + /** + * list streams with optional filtering and pagination + * + * @param params - optional parameters for filtering and pagination + * @returns a Promise that resolves to the list of streams + */ + async list(params?: ListStreamsParams): Promise { + const tracer = getTracer(); + const currentContext = context.active(); + + const span = tracer.startSpan( + 'agentuity.stream.list', + {}, + currentContext + ); + + try { + if (params?.limit !== undefined) { + if (params.limit <= 0 || params.limit > 1000) { + throw new Error('limit must be greater than 0 and less than 1000'); + } + span.setAttribute('limit', params.limit); + } + if (params?.offset !== undefined) { + span.setAttribute('offset', params.offset); + } + if (params?.name) { + span.setAttribute('name', params.name); + } + if (params?.metadata) { + span.setAttribute('metadata', JSON.stringify(params.metadata)); + } + + const spanContext = trace.setSpan(currentContext, span); + + return await context.with(spanContext, async () => { + const requestBody: Record = {}; + + if (params?.name) { + requestBody.name = params.name; + } + if (params?.metadata) { + requestBody.metadata = params.metadata; + } + if (params?.limit) { + requestBody.limit = params.limit; + } + if (params?.offset) { + requestBody.offset = params.offset; + } + + const resp = await POST( + '/list', + JSON.stringify(requestBody), + { + 'Content-Type': 'application/json', + }, + undefined, + undefined, + 'stream' + ); + + if (resp.status === 200) { + const result = resp.json as ListStreamsResponse; + span.setAttribute('stream.count', result.streams.length); + span.setAttribute('stream.total', result.total); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } + + if (resp.status === 400) { + const result = resp.json as ListStreamsResponse; + throw new Error( + result.message || `Bad request: ${resp.response.statusText}` + ); + } + + throw new Error( + `error listing streams: ${resp.response.statusText} (${resp.response.status})` + ); + }); + } catch (ex) { + recordException(span, ex); + span.setStatus({ code: SpanStatusCode.ERROR }); + throw ex; + } finally { + span.end(); + } + } + + /** + * delete a stream by id + * + * @param id - the stream id to delete + * @returns a Promise that resolves when the stream is deleted + */ + async delete(id: string): Promise { + if (!id || typeof id !== 'string' || id.trim().length === 0) { + throw new Error('Stream id is required and must be a non-empty string'); + } + + const tracer = getTracer(); + const currentContext = context.active(); + + const span = tracer.startSpan( + 'agentuity.stream.delete', + {}, + currentContext + ); + + try { + span.setAttribute('stream.id', id); + + const spanContext = trace.setSpan(currentContext, span); + + return await context.with(spanContext, async () => { + const resp = await DELETE( + `/${id}`, + undefined, + undefined, + undefined, + 'stream' + ); + + if (resp.status === 200 || resp.status === 204) { + span.setStatus({ code: SpanStatusCode.OK }); + return; + } + + if (resp.status === 404) { + throw new Error(`Stream not found: ${id}`); + } + + throw new Error( + `error deleting stream: ${resp.response.statusText} (${resp.response.status})` + ); + }); + } catch (ex) { + recordException(span, ex); + span.setStatus({ code: SpanStatusCode.ERROR }); + throw ex; + } finally { + span.end(); + } + } } diff --git a/src/types.ts b/src/types.ts index 09c0487..1c27d09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -353,6 +353,86 @@ export interface CreateStreamProps { compress?: true; } +/** + * Parameters for listing streams + */ +export interface ListStreamsParams { + /** + * optional name filter to search for streams + */ + name?: string; + + /** + * optional metadata filters to match streams + */ + metadata?: Record; + + /** + * maximum number of streams to return (default: 100, max: 1000) + */ + limit?: number; + + /** + * number of streams to skip for pagination + */ + offset?: number; +} + +/** + * Stream information returned by list operation + */ +export interface StreamInfo { + /** + * unique stream identifier + */ + id: string; + + /** + * the name of the stream + */ + name: string; + + /** + * the stream metadata + */ + metadata: Record; + + /** + * the public URL to access the stream + */ + url: string; + + /** + * the size of the stream in bytes + */ + sizeBytes: number; +} + +/** + * Response from listing streams + */ +export interface ListStreamsResponse { + /** + * whether the request was successful + */ + success: boolean; + + /** + * optional error message if not successful + */ + message?: string; + + /** + * array of streams matching the filter criteria + */ + streams: StreamInfo[]; + + /** + * total count of streams matching the filter (useful for pagination) + */ + total: number; +} + /** * A durable and resumable stream that can be written to and read many times. * The underlying stream is backed by a durable storage system and the URL @@ -413,6 +493,22 @@ export interface StreamAPI { * @returns a Promise that resolves to the created Stream */ create(name: string, props?: CreateStreamProps): Promise; + + /** + * list streams with optional filtering and pagination + * + * @param params - optional parameters for filtering and pagination + * @returns a Promise that resolves to the list of streams + */ + list(params?: ListStreamsParams): Promise; + + /** + * delete a stream by id + * + * @param id - the stream id to delete + * @returns a Promise that resolves when the stream is deleted + */ + delete(id: string): Promise; } type VectorUpsertEmbeddings = { diff --git a/test/stream.test.ts b/test/stream.test.ts index 1fb8f8d..fd06e16 100644 --- a/test/stream.test.ts +++ b/test/stream.test.ts @@ -1654,4 +1654,264 @@ describe('StreamAPI', () => { expect(errorThrown).toBe(true); }); }); + + describe('list', () => { + beforeEach(() => { + const mockFetch = mock( + async (url: URL | RequestInfo, options?: RequestInit) => { + if (options?.method === 'POST' && url.toString().includes('/list')) { + const body = JSON.parse(options.body as string); + + // Simulate different responses based on filters + if (body.limit && (body.limit <= 0 || body.limit > 1000)) { + const errorJson = { + success: false, + message: 'limit must be greater than 0 and less than 1000', + streams: [], + total: 0, + }; + return { + status: 400, + response: { + status: 400, + statusText: 'Bad Request', + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(errorJson), + }, + json: errorJson, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(errorJson), + }; + } + + // Mock successful response + const streams = [ + { + id: 'stream-1', + name: body.name || 'test-stream', + metadata: body.metadata || { type: 'test' }, + url: 'https://stream.test.com/stream-1', + sizeBytes: 1024, + }, + { + id: 'stream-2', + name: body.name || 'another-stream', + metadata: body.metadata || { type: 'test' }, + url: 'https://stream.test.com/stream-2', + sizeBytes: 2048, + }, + ]; + + const offset = body.offset || 0; + const limit = body.limit || 100; + const paginatedStreams = streams.slice(offset, offset + limit); + + const successJson = { + success: true, + streams: paginatedStreams, + total: streams.length, + }; + + return { + status: 200, + response: { + status: 200, + statusText: 'OK', + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(successJson), + }, + json: successJson, + headers: new Headers({ 'content-type': 'application/json' }), + json: () => Promise.resolve(successJson), + }; + } + + return { + status: 404, + response: { + status: 404, + statusText: 'Not Found', + headers: new Headers(), + }, + headers: new Headers(), + }; + } + ); + + setFetch(mockFetch as unknown as typeof fetch); + }); + + it('should list streams with no filters', async () => { + const result = await streamAPI.list(); + + expect(result.success).toBe(true); + expect(result.streams).toHaveLength(2); + expect(result.total).toBe(2); + expect(result.streams[0].id).toBe('stream-1'); + expect(result.streams[1].id).toBe('stream-2'); + }); + + it('should list streams with name filter', async () => { + const result = await streamAPI.list({ name: 'test-stream' }); + + expect(result.success).toBe(true); + expect(result.streams).toHaveLength(2); + expect(result.total).toBe(2); + }); + + it('should list streams with metadata filter', async () => { + const result = await streamAPI.list({ + metadata: { customerId: 'customer-123' }, + }); + + expect(result.success).toBe(true); + expect(result.streams).toHaveLength(2); + expect(result.total).toBe(2); + }); + + it('should list streams with limit', async () => { + const result = await streamAPI.list({ limit: 1 }); + + expect(result.success).toBe(true); + expect(result.streams).toHaveLength(1); + expect(result.streams[0].id).toBe('stream-1'); + }); + + it('should list streams with offset', async () => { + const result = await streamAPI.list({ offset: 1 }); + + expect(result.success).toBe(true); + expect(result.streams).toHaveLength(1); + expect(result.streams[0].id).toBe('stream-2'); + }); + + it('should validate limit range', async () => { + await expect(streamAPI.list({ limit: 0 })).rejects.toThrow( + 'limit must be greater than 0 and less than 1000' + ); + + await expect(streamAPI.list({ limit: 1001 })).rejects.toThrow( + 'limit must be greater than 0 and less than 1000' + ); + }); + + it('should accept valid limit values', async () => { + const result1 = await streamAPI.list({ limit: 1 }); + expect(result1.success).toBe(true); + + const result2 = await streamAPI.list({ limit: 1000 }); + expect(result2.success).toBe(true); + }); + + it('should return stream info with all required fields', async () => { + const result = await streamAPI.list(); + + expect(result.streams[0]).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + metadata: expect.any(Object), + url: expect.any(String), + sizeBytes: expect.any(Number), + }); + }); + + it('should list streams with combined filters', async () => { + const result = await streamAPI.list({ + name: 'test-stream', + metadata: { type: 'test' }, + limit: 10, + offset: 0, + }); + + expect(result.success).toBe(true); + expect(result.streams).toBeDefined(); + expect(result.total).toBeDefined(); + }); + }); + + describe('delete', () => { + beforeEach(() => { + const mockFetch = mock( + async (url: URL | RequestInfo, options?: RequestInit) => { + if (options?.method === 'DELETE') { + const urlStr = url.toString(); + + // Check for valid stream ID + if (urlStr.includes('/stream-123')) { + return { + status: 200, + response: { + status: 200, + statusText: 'OK', + headers: new Headers(), + }, + headers: new Headers(), + }; + } + + // Handle not found + if (urlStr.includes('/nonexistent')) { + return { + status: 404, + response: { + status: 404, + statusText: 'Not Found', + headers: new Headers(), + }, + headers: new Headers(), + }; + } + } + + return { + status: 404, + response: { + status: 404, + statusText: 'Not Found', + headers: new Headers(), + }, + headers: new Headers(), + }; + } + ); + + setFetch(mockFetch as unknown as typeof fetch); + }); + + it('should delete a stream successfully', async () => { + await expect(streamAPI.delete('stream-123')).resolves.toBeUndefined(); + }); + + it('should throw error for non-existent stream', async () => { + await expect(streamAPI.delete('nonexistent')).rejects.toThrow( + 'Stream not found: nonexistent' + ); + }); + + it('should validate stream id is required', async () => { + await expect(streamAPI.delete('')).rejects.toThrow( + 'Stream id is required and must be a non-empty string' + ); + }); + + it('should validate stream id is a string', async () => { + await expect( + streamAPI.delete(null as unknown as string) + ).rejects.toThrow( + 'Stream id is required and must be a non-empty string' + ); + + await expect( + streamAPI.delete(undefined as unknown as string) + ).rejects.toThrow( + 'Stream id is required and must be a non-empty string' + ); + }); + + it('should handle whitespace-only id', async () => { + await expect(streamAPI.delete(' ')).rejects.toThrow( + 'Stream id is required and must be a non-empty string' + ); + }); + }); }); From 83d6ef07045dc549dcd5a86609f7a5ebac6eae8d Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 21:06:18 -0500 Subject: [PATCH 2/7] fix all the lint failures --- package.json | 2 +- src/apis/patchportal.ts | 6 ++- src/apis/prompt/generic_types.ts | 10 ++--- src/apis/prompt/index.ts | 8 ++-- src/apis/stream.ts | 6 +-- src/autostart/index.ts | 25 +++++++---- src/index.ts | 4 +- src/otel/index.ts | 71 +++++++++++++++----------------- src/otel/logger.ts | 8 +++- src/utils/promptMetadata.ts | 6 +-- test/stream.test.ts | 14 ++----- test/utils/interpolate.test.ts | 4 +- 12 files changed, 84 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 53477bf..bb17c23 100644 --- a/package.json +++ b/package.json @@ -97,4 +97,4 @@ "mailparser": "^3.7.4", "nodemailer": "^7.0.3" } -} \ No newline at end of file +} diff --git a/src/apis/patchportal.ts b/src/apis/patchportal.ts index 501c875..da14876 100644 --- a/src/apis/patchportal.ts +++ b/src/apis/patchportal.ts @@ -65,7 +65,9 @@ export default class PatchPortal { internal.debug('🔍 State after set:', Object.keys(this.state)); internal.debug( '🔍 Data stored:', - typeof data === 'object' ? Object.keys(data as any) : typeof data + typeof data === 'object' + ? Object.keys(data as Record) + : typeof data ); } @@ -79,7 +81,7 @@ export default class PatchPortal { '🔍 Retrieved data:', result ? typeof result === 'object' - ? Object.keys(result as any) + ? Object.keys(result as Record) : typeof result : 'undefined' ); diff --git a/src/apis/prompt/generic_types.ts b/src/apis/prompt/generic_types.ts index 5999ad5..98cee84 100644 --- a/src/apis/prompt/generic_types.ts +++ b/src/apis/prompt/generic_types.ts @@ -38,12 +38,12 @@ export type Prompt< : Record); // Simple signature function type -export type PromptSignature> = ( - params: any -) => string; +export type PromptSignature< + _T extends Prompt, +> = (params: Record) => string; // Simple collection types -export type PromptsCollection = Record; -export type GetPromptSignatures> = { +export type PromptsCollection = Record; +export type GetPromptSignatures> = { [K in keyof T]: PromptSignature; }; diff --git a/src/apis/prompt/index.ts b/src/apis/prompt/index.ts index e9a6648..b08a4a9 100644 --- a/src/apis/prompt/index.ts +++ b/src/apis/prompt/index.ts @@ -1,8 +1,8 @@ // Main entry point for prompts - following POC pattern exactly -import fs from 'fs/promises'; -import path from 'path'; -import { pathToFileURL } from 'url'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { internal } from '../../logger/internal'; import { processPromptMetadataConcat } from '../../utils/promptMetadata'; @@ -106,7 +106,7 @@ export default class PromptAPI { const mtime = stats.mtime.getTime(); // Convert to file URL with cache-busting query param - const fileUrl = pathToFileURL(possiblePath).href + `?t=${mtime}`; + const fileUrl = `${pathToFileURL(possiblePath).href}?t=${mtime}`; // Use ESM dynamic import instead of require generatedModule = await import(fileUrl); diff --git a/src/apis/stream.ts b/src/apis/stream.ts index 2a204d1..0ee6abc 100644 --- a/src/apis/stream.ts +++ b/src/apis/stream.ts @@ -437,11 +437,7 @@ export default class StreamAPIImpl implements StreamAPI { const tracer = getTracer(); const currentContext = context.active(); - const span = tracer.startSpan( - 'agentuity.stream.list', - {}, - currentContext - ); + const span = tracer.startSpan('agentuity.stream.list', {}, currentContext); try { if (params?.limit !== undefined) { diff --git a/src/autostart/index.ts b/src/autostart/index.ts index c8448c6..07008f1 100644 --- a/src/autostart/index.ts +++ b/src/autostart/index.ts @@ -1,14 +1,18 @@ import yml from 'js-yaml'; import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { createResource, createUserLoggerProvider, registerOtel } from '../otel'; +import { + createResource, + createUserLoggerProvider, + registerOtel, +} from '../otel'; import { OtelLogger } from '../otel/logger'; import { createServer, createServerContext } from '../server'; import type { AgentConfig } from '../types'; import { Resource } from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; -import { LoggerProvider } from '@opentelemetry/sdk-logs'; -import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import type { LoggerProvider } from '@opentelemetry/sdk-logs'; +import type { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import type { LogRecordProcessor } from '@opentelemetry/sdk-logs'; /** @@ -89,7 +93,7 @@ export async function run(config: AutostartConfig) { const agentdir = data?.bundler?.agents?.dir; if (agentdir && existsSync(agentdir)) { config.agents = data.agents - .map((agent: { id: string; name: string; }) => { + .map((agent: { id: string; name: string }) => { const filename = join(agentdir, agent.name, 'index.ts'); if (existsSync(filename)) { return { @@ -125,9 +129,14 @@ export async function run(config: AutostartConfig) { environment: config.devmode ? 'development' : config.environment, }; const otel = registerOtel(otelConfig); - let userLoggerProvider: { provider: LoggerProvider; exporter: OTLPLogExporter; processor: LogRecordProcessor; } | undefined; + let userLoggerProvider: + | { + provider: LoggerProvider; + exporter: OTLPLogExporter; + processor: LogRecordProcessor; + } + | undefined; if (config.userOtelConf) { - const resource = new Resource({ ...createResource(otelConfig).attributes, ...config.userOtelConf.resourceAttributes, @@ -141,7 +150,9 @@ export async function run(config: AutostartConfig) { if (otel.logger instanceof OtelLogger) { otel.logger.addDelegate(userLoggerProvider.provider.getLogger('default')); } else { - console.warn('[WARN] user OTEL logger not attached: logger does not support addDelegate'); + console.warn( + '[WARN] user OTEL logger not attached: logger does not support addDelegate' + ); } } diff --git a/src/index.ts b/src/index.ts index 2e2dd4d..c6c08b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,9 @@ export async function runner( url: process.env.AGENTUITY_OTLP_URL, bearerToken: process.env.AGENTUITY_OTLP_BEARER_TOKEN, }, - userOtelConf: process.env.AGENTUITY_USER_OTEL_CONF ? JSON.parse(process.env.AGENTUITY_USER_OTEL_CONF) : undefined, + userOtelConf: process.env.AGENTUITY_USER_OTEL_CONF + ? JSON.parse(process.env.AGENTUITY_USER_OTEL_CONF) + : undefined, agents, }); } diff --git a/src/otel/index.ts b/src/otel/index.ts index f548621..ff7e5a6 100644 --- a/src/otel/index.ts +++ b/src/otel/index.ts @@ -68,7 +68,6 @@ interface OtelResponse { const devmodeExportInterval = 1_000; // 1 second const productionExportInterval = 10_000; // 10 seconds - export const createResource = (config: OtelConfig): Resource => { const { name, @@ -100,11 +99,10 @@ export const createAgentuityLoggerProvider = ({ headers, resource, }: { - url?: string, - headers?: Record, + url?: string; + headers?: Record; resource: Resource; }) => { - let processor: LogRecordProcessor; let exporter: OTLPLogExporter | undefined; @@ -117,9 +115,7 @@ export const createAgentuityLoggerProvider = ({ }); processor = new BatchLogRecordProcessor(exporter); } else { - processor = new SimpleLogRecordProcessor( - new ConsoleLogRecordExporter() - ); + processor = new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()); } const provider = new LoggerProvider({ resource, @@ -139,11 +135,11 @@ export const createUserLoggerProvider = ({ headers, resource, }: { - url: string, - headers?: Record, + url: string; + headers?: Record; resource: Resource; }) => { - let exporter = new OTLPLogExporter({ + const exporter = new OTLPLogExporter({ url: `${url}/v1/logs`, headers, compression: CompressionAlgorithm.GZIP, @@ -207,54 +203,53 @@ export function registerOtel(config: OtelConfig): OtelResponse { const traceExporter = url ? new OTLPTraceExporter({ - url: `${url}/v1/traces`, - headers, - keepAlive: true, - }) + url: `${url}/v1/traces`, + headers, + keepAlive: true, + }) : undefined; const metricExporter = url ? new OTLPMetricExporter({ - url: `${url}/v1/metrics`, - headers, - keepAlive: true, - }) + url: `${url}/v1/metrics`, + headers, + keepAlive: true, + }) : undefined; - // Create a separate metric reader for the NodeSDK const sdkMetricReader = url && metricExporter ? new PeriodicExportingMetricReader({ - exporter: metricExporter, - exportTimeoutMillis: devmode - ? devmodeExportInterval - : productionExportInterval, - exportIntervalMillis: devmode - ? devmodeExportInterval - : productionExportInterval, - }) + exporter: metricExporter, + exportTimeoutMillis: devmode + ? devmodeExportInterval + : productionExportInterval, + exportIntervalMillis: devmode + ? devmodeExportInterval + : productionExportInterval, + }) : undefined; // Create a separate metric reader for the MeterProvider const hostMetricReader = url && metricExporter ? new PeriodicExportingMetricReader({ - exporter: metricExporter, - exportTimeoutMillis: devmode - ? devmodeExportInterval - : productionExportInterval, - exportIntervalMillis: devmode - ? devmodeExportInterval - : productionExportInterval, - }) + exporter: metricExporter, + exportTimeoutMillis: devmode + ? devmodeExportInterval + : productionExportInterval, + exportIntervalMillis: devmode + ? devmodeExportInterval + : productionExportInterval, + }) : undefined; const meterProvider = hostMetricReader ? new MeterProvider({ - resource, - readers: [hostMetricReader], - }) + resource, + readers: [hostMetricReader], + }) : undefined; if (meterProvider) { diff --git a/src/otel/logger.ts b/src/otel/logger.ts index 2a7a313..e286579 100644 --- a/src/otel/logger.ts +++ b/src/otel/logger.ts @@ -58,10 +58,14 @@ export class OtelLogger implements Logger { return result; } - private emitToAll(severityNumber: LogsAPI.SeverityNumber, severityText: string, body: string) { + private emitToAll( + severityNumber: LogsAPI.SeverityNumber, + severityText: string, + body: string + ) { const attributes = this.getAttributes(); - this.delegates.forEach(delegate => { + this.delegates.forEach((delegate) => { try { delegate.emit({ severityNumber, diff --git a/src/utils/promptMetadata.ts b/src/utils/promptMetadata.ts index 9f3b58d..da074e6 100644 --- a/src/utils/promptMetadata.ts +++ b/src/utils/promptMetadata.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import PatchPortal from '../apis/patchportal.js'; import { internal } from '../logger/internal'; @@ -23,8 +23,8 @@ export async function processPromptMetadata( ): Promise { internal.debug('🔧 processPromptMetadata called with:', { slug: attributes.slug, - template: attributes.template?.substring(0, 50) + '...', - compiled: attributes.compiled?.substring(0, 50) + '...', + template: `${attributes.template?.substring(0, 50)}...`, + compiled: `${attributes.compiled?.substring(0, 50)}...`, variables: attributes.variables, }); diff --git a/test/stream.test.ts b/test/stream.test.ts index fd06e16..42f6c07 100644 --- a/test/stream.test.ts +++ b/test/stream.test.ts @@ -1678,7 +1678,6 @@ describe('StreamAPI', () => { headers: new Headers({ 'content-type': 'application/json' }), json: () => Promise.resolve(errorJson), }, - json: errorJson, headers: new Headers({ 'content-type': 'application/json' }), json: () => Promise.resolve(errorJson), }; @@ -1720,7 +1719,6 @@ describe('StreamAPI', () => { headers: new Headers({ 'content-type': 'application/json' }), json: () => Promise.resolve(successJson), }, - json: successJson, headers: new Headers({ 'content-type': 'application/json' }), json: () => Promise.resolve(successJson), }; @@ -1835,7 +1833,7 @@ describe('StreamAPI', () => { async (url: URL | RequestInfo, options?: RequestInit) => { if (options?.method === 'DELETE') { const urlStr = url.toString(); - + // Check for valid stream ID if (urlStr.includes('/stream-123')) { return { @@ -1848,7 +1846,7 @@ describe('StreamAPI', () => { headers: new Headers(), }; } - + // Handle not found if (urlStr.includes('/nonexistent')) { return { @@ -1895,17 +1893,13 @@ describe('StreamAPI', () => { }); it('should validate stream id is a string', async () => { - await expect( - streamAPI.delete(null as unknown as string) - ).rejects.toThrow( + await expect(streamAPI.delete(null as unknown as string)).rejects.toThrow( 'Stream id is required and must be a non-empty string' ); await expect( streamAPI.delete(undefined as unknown as string) - ).rejects.toThrow( - 'Stream id is required and must be a non-empty string' - ); + ).rejects.toThrow('Stream id is required and must be a non-empty string'); }); it('should handle whitespace-only id', async () => { diff --git a/test/utils/interpolate.test.ts b/test/utils/interpolate.test.ts index 4a4608c..4bfd2e2 100644 --- a/test/utils/interpolate.test.ts +++ b/test/utils/interpolate.test.ts @@ -63,12 +63,12 @@ describe('interpolateTemplate', () => { expectedErr: "Required variable 'test' not provided", }, { - input: 'this is a ${test}', + input: 'this is a $' + '{test}', variables: { test: null }, expectedVal: 'this is a $', }, { - input: 'this is a ${test:-foo}', + input: 'this is a $' + '{test:-foo}', variables: { test: 'foo' }, expectedVal: 'this is a $foo', }, From 443c6212aef3f0c0551b4ea8f819bc3cd095cc8f Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 21:09:12 -0500 Subject: [PATCH 3/7] fix the type --- src/apis/prompt/generic_types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apis/prompt/generic_types.ts b/src/apis/prompt/generic_types.ts index 98cee84..b24a868 100644 --- a/src/apis/prompt/generic_types.ts +++ b/src/apis/prompt/generic_types.ts @@ -44,6 +44,8 @@ export type PromptSignature< // Simple collection types export type PromptsCollection = Record; -export type GetPromptSignatures> = { +export type GetPromptSignatures< + T extends Record>, +> = { [K in keyof T]: PromptSignature; }; From 493f94ab72b7aeb8b5fd167146231b41ec1d3ee3 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 21:12:10 -0500 Subject: [PATCH 4/7] merge in changelog from previous commit --- .changeset/old-buttons-sing.md | 5 ----- CHANGELOG.md | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .changeset/old-buttons-sing.md diff --git a/.changeset/old-buttons-sing.md b/.changeset/old-buttons-sing.md deleted file mode 100644 index 24cd4f7..0000000 --- a/.changeset/old-buttons-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@agentuity/sdk": patch ---- - -Make the logging around Telemetry more generic diff --git a/CHANGELOG.md b/CHANGELOG.md index 9deb899..250dc50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Patch Changes +- b7c6499: Make the logging around Telemetry more generic + - You can now list and search through your streams with flexible filtering and pagination options. ```typescript From 9407ad87f9457d91ba81dbdaf37e4ca6b9336acc Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 21:57:58 -0500 Subject: [PATCH 5/7] feedback --- src/apis/patchportal.ts | 2 +- src/autostart/index.ts | 2 +- src/index.ts | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/apis/patchportal.ts b/src/apis/patchportal.ts index da14876..6010b35 100644 --- a/src/apis/patchportal.ts +++ b/src/apis/patchportal.ts @@ -65,7 +65,7 @@ export default class PatchPortal { internal.debug('🔍 State after set:', Object.keys(this.state)); internal.debug( '🔍 Data stored:', - typeof data === 'object' + data && typeof data === 'object' ? Object.keys(data as Record) : typeof data ); diff --git a/src/autostart/index.ts b/src/autostart/index.ts index 07008f1..755138e 100644 --- a/src/autostart/index.ts +++ b/src/autostart/index.ts @@ -18,7 +18,7 @@ import type { LogRecordProcessor } from '@opentelemetry/sdk-logs'; /** * Configuration for user provided OpenTelemetry */ -interface UserOpenTelemetryConfig { +export interface UserOpenTelemetryConfig { endpoint: string; // only supports http/json for now // protocol: 'grpc' | 'http/protobuf' | 'http/json'; diff --git a/src/index.ts b/src/index.ts index c6c08b7..7ca7bfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ import StreamAPIImpl from './apis/stream'; export { EmailAPI, DiscordAPI, PatchPortal, PromptAPI, StreamAPIImpl }; import { TeamsActivityHandler } from 'botbuilder'; -import { run } from './autostart'; +import { run, type UserOpenTelemetryConfig } from './autostart'; import { UnsupportedSlackPayload } from './io/slack'; import { AgentuityTeamsActivityHandler } from './io/teams/AgentuityTeamsActivityHandler'; import { AgentuityTeamsAdapter } from './io/teams/AgentuityTeamsAdapter'; @@ -51,6 +51,16 @@ export async function runner( '[WARN] expected AGENTUITY_CLOUD_AGENTS_JSON to be set but it was not. will attempt to load manually.' ); } + let userOtelConf: UserOpenTelemetryConfig | undefined; + if (process.env.AGENTUITY_USER_OTEL_CONF) { + try { + userOtelConf = JSON.parse(process.env.AGENTUITY_USER_OTEL_CONF); + } catch (error) { + console.warn( + `[WARN] Failed to parse AGENTUITY_USER_OTEL_CONF: ${error instanceof Error ? error.message : String(error)}` + ); + } + } await run({ basedir: dir, orgId: process.env.AGENTUITY_CLOUD_ORG_ID, @@ -71,9 +81,7 @@ export async function runner( url: process.env.AGENTUITY_OTLP_URL, bearerToken: process.env.AGENTUITY_OTLP_BEARER_TOKEN, }, - userOtelConf: process.env.AGENTUITY_USER_OTEL_CONF - ? JSON.parse(process.env.AGENTUITY_USER_OTEL_CONF) - : undefined, + userOtelConf, agents, }); } From 5247e9cd1daae56ba8a5c7c50aed5b5f8837fae7 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 23:02:49 -0500 Subject: [PATCH 6/7] fixed delete --- src/apis/stream.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apis/stream.ts b/src/apis/stream.ts index 0ee6abc..e68bcf6 100644 --- a/src/apis/stream.ts +++ b/src/apis/stream.ts @@ -544,6 +544,7 @@ export default class StreamAPIImpl implements StreamAPI { undefined, undefined, undefined, + undefined, 'stream' ); From a47f948934b0f6ded75e645610d11bcd168ecce6 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 15 Oct 2025 23:13:06 -0500 Subject: [PATCH 7/7] fixes --- src/apis/stream.ts | 4 +++- test/stream.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/apis/stream.ts b/src/apis/stream.ts index e68bcf6..fbae7b3 100644 --- a/src/apis/stream.ts +++ b/src/apis/stream.ts @@ -442,7 +442,9 @@ export default class StreamAPIImpl implements StreamAPI { try { if (params?.limit !== undefined) { if (params.limit <= 0 || params.limit > 1000) { - throw new Error('limit must be greater than 0 and less than 1000'); + throw new Error( + 'limit must be greater than 0 and less than or equal to 1000' + ); } span.setAttribute('limit', params.limit); } diff --git a/test/stream.test.ts b/test/stream.test.ts index 42f6c07..48ad678 100644 --- a/test/stream.test.ts +++ b/test/stream.test.ts @@ -1785,11 +1785,11 @@ describe('StreamAPI', () => { it('should validate limit range', async () => { await expect(streamAPI.list({ limit: 0 })).rejects.toThrow( - 'limit must be greater than 0 and less than 1000' + 'limit must be greater than 0 and less than or equal to 1000' ); await expect(streamAPI.list({ limit: 1001 })).rejects.toThrow( - 'limit must be greater than 0 and less than 1000' + 'limit must be greater than 0 and less than or equal to 1000' ); });