@yuliaedomskikh/agentflow-client 1.1.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.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @yuliaedomskikh/agentflow-client
2
+
3
+ > Published npm package: **`@yuliaedomskikh/agentflow-client`**.
4
+
5
+ Install:
6
+
7
+ ```bash
8
+ npm install @yuliaedomskikh/agentflow-client
9
+ ```
10
+
11
+ ```ts
12
+ import { AgentFlowClient } from "@yuliaedomskikh/agentflow-client";
13
+ const client = new AgentFlowClient("http://localhost:8000", "dev-key");
14
+ const order = await client.getOrder("ORD-20260404-1001");
15
+ console.log(order.status, (await client.getMetric("revenue", "24h")).value);
16
+ ```
17
+
18
+ ```ts
19
+ import {
20
+ AgentFlowClient,
21
+ RetryPolicy,
22
+ } from "@yuliaedomskikh/agentflow-client";
23
+
24
+ const client = new AgentFlowClient("http://localhost:8000", "dev-key");
25
+ client.configureResilience({
26
+ retryPolicy: new RetryPolicy({ maxAttempts: 5 }),
27
+ });
28
+ ```
@@ -0,0 +1,7 @@
1
+ export { AgentFlowClient } from "./src/client.js";
2
+ export { CircuitBreaker, CircuitOpenError, CircuitState, } from "./src/circuitBreaker.js";
3
+ export { AgentFlowError, AuthError, DataFreshnessError, EntityNotFoundError, RateLimitError, } from "./src/exceptions.js";
4
+ export { RetryPolicy } from "./src/retry.js";
5
+ export type { BatchItem, BatchResponse, CatalogResponse, ClientOptions, EventFilters, HealthStatus, MetricName, MetricResult, OrderEntity, PipelineEvent, ProductEntity, QueryResult, SessionEntity, TimeWindow, UserEntity, } from "./src/models.js";
6
+ export type { CircuitBreakerOptions } from "./src/circuitBreaker.js";
7
+ export type { RetryPolicyOptions } from "./src/retry.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { AgentFlowClient } from "./src/client.js";
2
+ export { CircuitBreaker, CircuitOpenError, CircuitState, } from "./src/circuitBreaker.js";
3
+ export { AgentFlowError, AuthError, DataFreshnessError, EntityNotFoundError, RateLimitError, } from "./src/exceptions.js";
4
+ export { RetryPolicy } from "./src/retry.js";
@@ -0,0 +1,28 @@
1
+ import { AgentFlowError } from "./exceptions.js";
2
+ export declare enum CircuitState {
3
+ CLOSED = "closed",
4
+ OPEN = "open",
5
+ HALF_OPEN = "half_open"
6
+ }
7
+ export declare class CircuitOpenError extends AgentFlowError {
8
+ constructor(message?: string);
9
+ }
10
+ export interface CircuitBreakerOptions {
11
+ failureThreshold?: number;
12
+ resetTimeoutMs?: number;
13
+ halfOpenMaxCalls?: number;
14
+ }
15
+ export declare class CircuitBreaker {
16
+ readonly failureThreshold: number;
17
+ readonly resetTimeoutMs: number;
18
+ readonly halfOpenMaxCalls: number;
19
+ private _state;
20
+ private failureCount;
21
+ private openedAt;
22
+ private halfOpenCalls;
23
+ constructor(options?: CircuitBreakerOptions);
24
+ beforeCall(): void;
25
+ recordSuccess(): void;
26
+ recordFailure(): void;
27
+ get state(): CircuitState;
28
+ }
@@ -0,0 +1,66 @@
1
+ import { AgentFlowError } from "./exceptions.js";
2
+ export var CircuitState;
3
+ (function (CircuitState) {
4
+ CircuitState["CLOSED"] = "closed";
5
+ CircuitState["OPEN"] = "open";
6
+ CircuitState["HALF_OPEN"] = "half_open";
7
+ })(CircuitState || (CircuitState = {}));
8
+ export class CircuitOpenError extends AgentFlowError {
9
+ constructor(message = "circuit is open") {
10
+ super(message);
11
+ this.name = "CircuitOpenError";
12
+ }
13
+ }
14
+ export class CircuitBreaker {
15
+ failureThreshold;
16
+ resetTimeoutMs;
17
+ halfOpenMaxCalls;
18
+ _state = CircuitState.CLOSED;
19
+ failureCount = 0;
20
+ openedAt = 0;
21
+ halfOpenCalls = 0;
22
+ constructor(options = {}) {
23
+ this.failureThreshold = options.failureThreshold ?? 5;
24
+ this.resetTimeoutMs = options.resetTimeoutMs ?? 30_000;
25
+ this.halfOpenMaxCalls = options.halfOpenMaxCalls ?? 1;
26
+ }
27
+ beforeCall() {
28
+ if (this._state === CircuitState.OPEN) {
29
+ if (Date.now() - this.openedAt >= this.resetTimeoutMs) {
30
+ this._state = CircuitState.HALF_OPEN;
31
+ this.halfOpenCalls = 0;
32
+ }
33
+ else {
34
+ throw new CircuitOpenError("circuit is open");
35
+ }
36
+ }
37
+ if (this._state === CircuitState.HALF_OPEN) {
38
+ if (this.halfOpenCalls >= this.halfOpenMaxCalls) {
39
+ throw new CircuitOpenError("circuit is half-open, probe in flight");
40
+ }
41
+ this.halfOpenCalls += 1;
42
+ }
43
+ }
44
+ recordSuccess() {
45
+ this._state = CircuitState.CLOSED;
46
+ this.failureCount = 0;
47
+ this.halfOpenCalls = 0;
48
+ }
49
+ recordFailure() {
50
+ if (this._state === CircuitState.HALF_OPEN) {
51
+ this._state = CircuitState.OPEN;
52
+ this.openedAt = Date.now();
53
+ this.halfOpenCalls = 0;
54
+ return;
55
+ }
56
+ this.failureCount += 1;
57
+ if (this.failureCount >= this.failureThreshold) {
58
+ this._state = CircuitState.OPEN;
59
+ this.openedAt = Date.now();
60
+ this.halfOpenCalls = 0;
61
+ }
62
+ }
63
+ get state() {
64
+ return this._state;
65
+ }
66
+ }
@@ -0,0 +1,55 @@
1
+ import type { BatchItem, BatchResponse, CatalogResponse, EventFilters, FetchLike, HealthStatus, MetricName, MetricResult, OrderEntity, PipelineEvent, ProductEntity, QueryResult, SessionEntity, TimeWindow, UserEntity } from "./models.js";
2
+ import { CircuitBreaker } from "./circuitBreaker.js";
3
+ import { RetryPolicy } from "./retry.js";
4
+ export declare class AgentFlowClient {
5
+ private readonly baseUrl;
6
+ private readonly apiKey;
7
+ private readonly fetchImpl;
8
+ private readonly timeoutMs;
9
+ private readonly headers;
10
+ private readonly contractVersions;
11
+ private readonly contractCache;
12
+ retryPolicy: RetryPolicy;
13
+ circuitBreaker: CircuitBreaker;
14
+ constructor(baseUrl: string, apiKey: string, options?: {
15
+ fetch?: FetchLike;
16
+ timeoutMs?: number;
17
+ headers?: HeadersInit;
18
+ contractVersion?: string;
19
+ });
20
+ configureResilience(options: {
21
+ retryPolicy?: RetryPolicy;
22
+ circuitBreaker?: CircuitBreaker;
23
+ }): this;
24
+ getOrder(orderId: string): Promise<OrderEntity>;
25
+ getUser(userId: string): Promise<UserEntity>;
26
+ getProduct(productId: string): Promise<ProductEntity>;
27
+ getSession(sessionId: string): Promise<SessionEntity>;
28
+ getMetric(name: MetricName | string, window?: TimeWindow | string): Promise<MetricResult>;
29
+ query(question: string): Promise<QueryResult>;
30
+ health(): Promise<HealthStatus>;
31
+ isFresh(maxAgeSeconds?: number): Promise<boolean>;
32
+ catalog(): Promise<CatalogResponse>;
33
+ streamEvents(filters?: EventFilters): AsyncGenerator<PipelineEvent>;
34
+ batch(requests: BatchItem[]): Promise<BatchResponse>;
35
+ batchEntity(entityType: string, entityId: string, requestId?: string): BatchItem;
36
+ batchMetric(name: MetricName | string, window?: TimeWindow | string, requestId?: string): BatchItem;
37
+ batchQuery(question: string, context?: Record<string, unknown>, requestId?: string): BatchItem;
38
+ private resolveFetch;
39
+ private parseContractVersions;
40
+ private getEntity;
41
+ private applyContractVersion;
42
+ private getContract;
43
+ private requestJson;
44
+ private fetchResponse;
45
+ private throwHttpError;
46
+ private readJson;
47
+ private errorDetail;
48
+ private extractFreshnessSeconds;
49
+ private normalizeEntity;
50
+ private normalizeQueryMetadata;
51
+ private toNumber;
52
+ private toNullableNumber;
53
+ private objectHeaders;
54
+ private requestId;
55
+ }
@@ -0,0 +1,375 @@
1
+ import { AgentFlowError, AuthError, DataFreshnessError, EntityNotFoundError, RateLimitError, } from "./exceptions.js";
2
+ import { CircuitBreaker } from "./circuitBreaker.js";
3
+ import { RETRYABLE_STATUS, RetryPolicy, isRetryableMethod, } from "./retry.js";
4
+ import { streamSseJson } from "./stream.js";
5
+ const ENTITY_NUMBER_FIELDS = {
6
+ order: ["total_amount"],
7
+ user: ["total_orders", "total_spent"],
8
+ product: ["price", "stock_quantity"],
9
+ session: ["duration_seconds", "event_count", "unique_pages"],
10
+ };
11
+ export class AgentFlowClient {
12
+ baseUrl;
13
+ apiKey;
14
+ fetchImpl;
15
+ timeoutMs;
16
+ headers;
17
+ contractVersions;
18
+ contractCache = new Map();
19
+ retryPolicy;
20
+ circuitBreaker;
21
+ constructor(baseUrl, apiKey, options = {}) {
22
+ const legacyOptions = options;
23
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
24
+ this.apiKey = apiKey;
25
+ this.fetchImpl = options.fetch ?? this.resolveFetch();
26
+ this.timeoutMs = options.timeoutMs ?? 10_000;
27
+ this.headers = options.headers;
28
+ this.contractVersions = this.parseContractVersions(options.contractVersion);
29
+ this.retryPolicy = legacyOptions.retryPolicy ?? new RetryPolicy();
30
+ this.circuitBreaker = legacyOptions.circuitBreaker ?? new CircuitBreaker();
31
+ }
32
+ configureResilience(options) {
33
+ if (options.retryPolicy) {
34
+ this.retryPolicy = options.retryPolicy;
35
+ }
36
+ if (options.circuitBreaker) {
37
+ this.circuitBreaker = options.circuitBreaker;
38
+ }
39
+ return this;
40
+ }
41
+ async getOrder(orderId) {
42
+ return this.getEntity("order", orderId);
43
+ }
44
+ async getUser(userId) {
45
+ return this.getEntity("user", userId);
46
+ }
47
+ async getProduct(productId) {
48
+ return this.getEntity("product", productId);
49
+ }
50
+ async getSession(sessionId) {
51
+ return this.getEntity("session", sessionId);
52
+ }
53
+ async getMetric(name, window = "1h") {
54
+ const payload = await this.requestJson("GET", `/v1/metrics/${encodeURIComponent(name)}`, { params: { window } });
55
+ return {
56
+ ...payload,
57
+ value: this.toNumber(payload.value),
58
+ components: payload.components ?? null,
59
+ };
60
+ }
61
+ async query(question) {
62
+ const payload = await this.requestJson("POST", "/v1/query", {
63
+ json: { question },
64
+ });
65
+ return {
66
+ ...payload,
67
+ metadata: this.normalizeQueryMetadata(payload.metadata ?? {}),
68
+ };
69
+ }
70
+ async health() {
71
+ const payload = await this.requestJson("GET", "/v1/health");
72
+ return {
73
+ ...payload,
74
+ freshness_seconds: this.extractFreshnessSeconds(payload.components),
75
+ };
76
+ }
77
+ async isFresh(maxAgeSeconds = 60) {
78
+ const health = await this.health();
79
+ if (health.status !== "healthy") {
80
+ throw new DataFreshnessError(`Pipeline is ${health.status}; freshness check cannot be trusted`);
81
+ }
82
+ if (health.freshness_seconds == null) {
83
+ throw new DataFreshnessError("Pipeline freshness metric is unavailable");
84
+ }
85
+ return health.freshness_seconds < maxAgeSeconds;
86
+ }
87
+ async catalog() {
88
+ return this.requestJson("GET", "/v1/catalog");
89
+ }
90
+ streamEvents(filters = {}) {
91
+ const self = this;
92
+ return (async function* () {
93
+ const response = await self.fetchResponse("GET", "/v1/stream/events", {
94
+ params: {
95
+ event_type: filters.eventType,
96
+ entity_id: filters.entityId,
97
+ },
98
+ accept: "text/event-stream",
99
+ signal: filters.signal,
100
+ });
101
+ yield* streamSseJson(response, filters.signal);
102
+ })();
103
+ }
104
+ async batch(requests) {
105
+ return this.requestJson("POST", "/v1/batch", {
106
+ json: { requests },
107
+ });
108
+ }
109
+ batchEntity(entityType, entityId, requestId) {
110
+ return {
111
+ id: requestId ?? this.requestId("entity"),
112
+ type: "entity",
113
+ params: {
114
+ entity_type: entityType,
115
+ entity_id: entityId,
116
+ },
117
+ };
118
+ }
119
+ batchMetric(name, window = "1h", requestId) {
120
+ return {
121
+ id: requestId ?? this.requestId("metric"),
122
+ type: "metric",
123
+ params: { name, window },
124
+ };
125
+ }
126
+ batchQuery(question, context, requestId) {
127
+ return {
128
+ id: requestId ?? this.requestId("query"),
129
+ type: "query",
130
+ params: context == null ? { question } : { question, context },
131
+ };
132
+ }
133
+ resolveFetch() {
134
+ if (typeof fetch !== "function") {
135
+ throw new AgentFlowError("Global fetch is unavailable. Pass options.fetch when constructing AgentFlowClient.");
136
+ }
137
+ return fetch.bind(globalThis);
138
+ }
139
+ parseContractVersions(contractVersion) {
140
+ if (!contractVersion) {
141
+ return {};
142
+ }
143
+ const [entity, rawVersion] = contractVersion.split(":", 2);
144
+ if (!entity || !rawVersion) {
145
+ throw new Error("contractVersion must use '<entity>:<version>' format.");
146
+ }
147
+ return {
148
+ [entity]: rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion,
149
+ };
150
+ }
151
+ async getEntity(entityType, entityId) {
152
+ const envelope = await this.requestJson("GET", `/v1/entity/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`);
153
+ const versioned = await this.applyContractVersion(entityType, envelope.data);
154
+ return this.normalizeEntity(entityType, versioned);
155
+ }
156
+ async applyContractVersion(entityType, payload) {
157
+ const version = this.contractVersions[entityType];
158
+ if (!version) {
159
+ return payload;
160
+ }
161
+ const contract = await this.getContract(entityType, version);
162
+ const requiredFields = contract.fields
163
+ .filter((field) => field.required)
164
+ .map((field) => field.name);
165
+ const missingFields = requiredFields.filter((field) => !(field in payload));
166
+ if (missingFields.length > 0) {
167
+ throw new AgentFlowError("Contract validation failed. Missing required fields: "
168
+ + missingFields.join(", "));
169
+ }
170
+ const allowedFields = new Set(contract.fields.map((field) => field.name));
171
+ const filteredEntries = Object.entries(payload).filter(([key]) => allowedFields.has(key));
172
+ return Object.fromEntries(filteredEntries);
173
+ }
174
+ async getContract(entityType, version) {
175
+ const cacheKey = `${entityType}:${version}`;
176
+ const cached = this.contractCache.get(cacheKey);
177
+ if (cached) {
178
+ return cached;
179
+ }
180
+ const contract = await this.requestJson("GET", `/v1/contracts/${encodeURIComponent(entityType)}/${encodeURIComponent(version)}`);
181
+ this.contractCache.set(cacheKey, contract);
182
+ return contract;
183
+ }
184
+ async requestJson(method, path, options = {}) {
185
+ const response = await this.fetchResponse(method, path, options);
186
+ const payload = await this.readJson(response);
187
+ return payload;
188
+ }
189
+ async fetchResponse(method, path, options = {}) {
190
+ const url = new URL(`${this.baseUrl}${path}`);
191
+ for (const [key, value] of Object.entries(options.params ?? {})) {
192
+ if (value != null) {
193
+ url.searchParams.set(key, String(value));
194
+ }
195
+ }
196
+ const headers = {
197
+ Accept: options.accept ?? "application/json",
198
+ "X-API-Key": this.apiKey,
199
+ ...(options.json ? { "Content-Type": "application/json" } : {}),
200
+ ...this.objectHeaders(this.headers),
201
+ };
202
+ const canRetry = isRetryableMethod(method, headers);
203
+ let attempt = 0;
204
+ this.circuitBreaker.beforeCall();
205
+ while (true) {
206
+ const controller = new AbortController();
207
+ const onAbort = () => controller.abort();
208
+ options.signal?.addEventListener("abort", onAbort, { once: true });
209
+ const timeoutId = this.timeoutMs > 0
210
+ ? setTimeout(() => controller.abort(), this.timeoutMs)
211
+ : undefined;
212
+ try {
213
+ const response = await this.fetchImpl(url.toString(), {
214
+ method,
215
+ headers,
216
+ body: options.json ? JSON.stringify(options.json) : undefined,
217
+ signal: controller.signal,
218
+ });
219
+ const retryAfterHeader = response.headers.get("retry-after");
220
+ const retryAfterSeconds = retryAfterHeader == null
221
+ ? undefined
222
+ : Number(retryAfterHeader);
223
+ if (canRetry
224
+ && RETRYABLE_STATUS.has(response.status)
225
+ && attempt < this.retryPolicy.maxAttempts - 1) {
226
+ const delayMs = this.retryPolicy.computeDelay(attempt, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds * 1_000 : undefined);
227
+ attempt += 1;
228
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
229
+ continue;
230
+ }
231
+ if (response.status >= 500) {
232
+ this.circuitBreaker.recordFailure();
233
+ }
234
+ else {
235
+ this.circuitBreaker.recordSuccess();
236
+ }
237
+ if (!response.ok) {
238
+ await this.throwHttpError(response, path);
239
+ }
240
+ return response;
241
+ }
242
+ catch (error) {
243
+ if (error instanceof AgentFlowError
244
+ || error instanceof AuthError
245
+ || error instanceof RateLimitError
246
+ || error instanceof EntityNotFoundError) {
247
+ throw error;
248
+ }
249
+ const timedOut = controller.signal.aborted && !options.signal?.aborted;
250
+ const userAborted = controller.signal.aborted && !!options.signal?.aborted;
251
+ if (!userAborted && canRetry && attempt < this.retryPolicy.maxAttempts - 1) {
252
+ const delayMs = this.retryPolicy.computeDelay(attempt);
253
+ attempt += 1;
254
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
255
+ continue;
256
+ }
257
+ if (!userAborted) {
258
+ this.circuitBreaker.recordFailure();
259
+ }
260
+ const message = timedOut
261
+ ? `Request timed out after ${this.timeoutMs}ms`
262
+ : error instanceof Error
263
+ ? error.message
264
+ : "Unknown request error";
265
+ throw new AgentFlowError(`Request failed: ${message}`);
266
+ }
267
+ finally {
268
+ if (timeoutId !== undefined) {
269
+ clearTimeout(timeoutId);
270
+ }
271
+ options.signal?.removeEventListener("abort", onAbort);
272
+ }
273
+ }
274
+ }
275
+ async throwHttpError(response, path) {
276
+ const payload = await this.readJson(response, true);
277
+ const detail = this.errorDetail(payload, response);
278
+ if (response.status === 401) {
279
+ throw new AuthError(detail ?? "Unauthorized");
280
+ }
281
+ if (response.status === 429) {
282
+ const retryAfterHeader = response.headers.get("retry-after");
283
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : 0;
284
+ throw new RateLimitError(detail ?? "Rate limit exceeded", retryAfter || 0);
285
+ }
286
+ if (response.status === 404) {
287
+ const parts = path.split("/").filter(Boolean);
288
+ if (parts.length >= 4 && parts[1] === "entity") {
289
+ throw new EntityNotFoundError(parts[2], parts[3], detail ?? undefined);
290
+ }
291
+ }
292
+ throw new AgentFlowError(detail ?? response.statusText, response.status);
293
+ }
294
+ async readJson(response, allowEmpty = false) {
295
+ const contentType = response.headers.get("content-type") ?? "";
296
+ if (!contentType.includes("application/json")) {
297
+ if (allowEmpty) {
298
+ return {};
299
+ }
300
+ throw new AgentFlowError(`Expected JSON response but received '${contentType || "unknown"}'`);
301
+ }
302
+ const payload = (await response.json());
303
+ if (payload && typeof payload === "object") {
304
+ return payload;
305
+ }
306
+ if (allowEmpty) {
307
+ return {};
308
+ }
309
+ throw new AgentFlowError("Response payload is not an object");
310
+ }
311
+ errorDetail(payload, response) {
312
+ const detail = payload.detail;
313
+ if (typeof detail === "string") {
314
+ return detail;
315
+ }
316
+ return response.statusText || undefined;
317
+ }
318
+ extractFreshnessSeconds(components) {
319
+ for (const component of components) {
320
+ if (component.name === "freshness") {
321
+ const value = component.metrics.last_event_age_seconds;
322
+ return typeof value === "number" ? value : value == null ? null : Number(value);
323
+ }
324
+ }
325
+ return null;
326
+ }
327
+ normalizeEntity(entityType, entity) {
328
+ const normalized = { ...entity };
329
+ for (const field of ENTITY_NUMBER_FIELDS[entityType] ?? []) {
330
+ if (field in normalized) {
331
+ normalized[field] = this.toNullableNumber(normalized[field]);
332
+ }
333
+ }
334
+ return normalized;
335
+ }
336
+ normalizeQueryMetadata(metadata) {
337
+ const normalized = { ...metadata };
338
+ for (const field of [
339
+ "rows_returned",
340
+ "execution_time_ms",
341
+ "data_freshness_seconds",
342
+ ]) {
343
+ if (field in normalized) {
344
+ normalized[field] = this.toNullableNumber(normalized[field]);
345
+ }
346
+ }
347
+ return normalized;
348
+ }
349
+ toNumber(value) {
350
+ return typeof value === "number" ? value : Number(value);
351
+ }
352
+ toNullableNumber(value) {
353
+ if (value == null) {
354
+ return null;
355
+ }
356
+ return this.toNumber(value);
357
+ }
358
+ objectHeaders(headers) {
359
+ if (!headers) {
360
+ return {};
361
+ }
362
+ if (headers instanceof Headers) {
363
+ return Object.fromEntries(headers.entries());
364
+ }
365
+ if (Array.isArray(headers)) {
366
+ return Object.fromEntries(headers);
367
+ }
368
+ return headers;
369
+ }
370
+ requestId(prefix) {
371
+ const token = globalThis.crypto?.randomUUID?.().replace(/-/g, "").slice(0, 8)
372
+ ?? Math.random().toString(16).slice(2, 10);
373
+ return `${prefix}-${token}`;
374
+ }
375
+ }
@@ -0,0 +1,19 @@
1
+ export declare class AgentFlowError extends Error {
2
+ readonly statusCode?: number;
3
+ constructor(message: string, statusCode?: number);
4
+ }
5
+ export declare class AuthError extends AgentFlowError {
6
+ constructor(message?: string);
7
+ }
8
+ export declare class RateLimitError extends AgentFlowError {
9
+ readonly retryAfter: number;
10
+ constructor(message?: string, retryAfter?: number);
11
+ }
12
+ export declare class DataFreshnessError extends AgentFlowError {
13
+ constructor(message: string);
14
+ }
15
+ export declare class EntityNotFoundError extends AgentFlowError {
16
+ readonly entityType: string;
17
+ readonly entityId: string;
18
+ constructor(entityType: string, entityId: string, message?: string);
19
+ }
@@ -0,0 +1,38 @@
1
+ export class AgentFlowError extends Error {
2
+ statusCode;
3
+ constructor(message, statusCode) {
4
+ super(message);
5
+ this.name = "AgentFlowError";
6
+ this.statusCode = statusCode;
7
+ }
8
+ }
9
+ export class AuthError extends AgentFlowError {
10
+ constructor(message = "Unauthorized") {
11
+ super(message, 401);
12
+ this.name = "AuthError";
13
+ }
14
+ }
15
+ export class RateLimitError extends AgentFlowError {
16
+ retryAfter;
17
+ constructor(message = "Rate limit exceeded", retryAfter = 0) {
18
+ super(message, 429);
19
+ this.name = "RateLimitError";
20
+ this.retryAfter = retryAfter;
21
+ }
22
+ }
23
+ export class DataFreshnessError extends AgentFlowError {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "DataFreshnessError";
27
+ }
28
+ }
29
+ export class EntityNotFoundError extends AgentFlowError {
30
+ entityType;
31
+ entityId;
32
+ constructor(entityType, entityId, message) {
33
+ super(message ?? `${entityType}/${entityId} not found`, 404);
34
+ this.name = "EntityNotFoundError";
35
+ this.entityType = entityType;
36
+ this.entityId = entityId;
37
+ }
38
+ }
@@ -0,0 +1,157 @@
1
+ import type { CircuitBreaker } from "./circuitBreaker.js";
2
+ import type { RetryPolicy } from "./retry.js";
3
+ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
4
+ export interface ClientOptions {
5
+ fetch?: FetchLike;
6
+ timeoutMs?: number;
7
+ headers?: HeadersInit;
8
+ contractVersion?: string;
9
+ retryPolicy?: RetryPolicy;
10
+ circuitBreaker?: CircuitBreaker;
11
+ }
12
+ export type MetricName = "revenue" | "order_count" | "avg_order_value" | "conversion_rate" | "active_sessions" | "error_rate";
13
+ export type TimeWindow = "5m" | "15m" | "1h" | "6h" | "24h" | "7d" | "now";
14
+ export interface EntityEnvelope<TData extends Record<string, unknown>> {
15
+ entity_type: string;
16
+ entity_id: string;
17
+ data: TData;
18
+ last_updated: string | null;
19
+ freshness_seconds: number | null;
20
+ meta?: Record<string, unknown>;
21
+ }
22
+ export interface OrderEntity {
23
+ order_id: string;
24
+ user_id: string;
25
+ status: string;
26
+ total_amount: number;
27
+ currency: string;
28
+ created_at: string;
29
+ is_overdue?: boolean;
30
+ }
31
+ export interface UserEntity {
32
+ user_id: string;
33
+ total_orders: number;
34
+ total_spent: number;
35
+ first_order_at: string;
36
+ last_order_at: string;
37
+ preferred_category: string;
38
+ }
39
+ export interface ProductEntity {
40
+ product_id: string;
41
+ name: string;
42
+ category: string;
43
+ price: number;
44
+ in_stock: boolean;
45
+ stock_quantity: number;
46
+ }
47
+ export interface SessionEntity {
48
+ session_id: string;
49
+ user_id: string | null;
50
+ started_at: string;
51
+ ended_at: string | null;
52
+ duration_seconds: number | null;
53
+ event_count: number;
54
+ unique_pages: number;
55
+ funnel_stage: string;
56
+ is_conversion: boolean;
57
+ }
58
+ export interface MetricResult {
59
+ metric_name: string;
60
+ value: number;
61
+ unit: string;
62
+ window: string;
63
+ computed_at: string;
64
+ components: Record<string, unknown> | null;
65
+ meta?: Record<string, unknown>;
66
+ }
67
+ export interface QueryMetadata {
68
+ rows_returned?: number;
69
+ execution_time_ms?: number;
70
+ data_freshness_seconds?: number | null;
71
+ [key: string]: unknown;
72
+ }
73
+ export interface QueryResult {
74
+ answer: Record<string, unknown> | Array<Record<string, unknown>>;
75
+ sql: string | null;
76
+ metadata: QueryMetadata;
77
+ }
78
+ export interface HealthComponent {
79
+ name: string;
80
+ status: string;
81
+ message: string;
82
+ metrics: Record<string, unknown>;
83
+ source: string;
84
+ }
85
+ export interface HealthStatus {
86
+ status: string;
87
+ checked_at: string;
88
+ components: HealthComponent[];
89
+ freshness_seconds: number | null;
90
+ }
91
+ export interface CatalogEntity {
92
+ description: string;
93
+ fields: Record<string, string>;
94
+ primary_key: string;
95
+ contract_version?: string | null;
96
+ }
97
+ export interface CatalogMetric {
98
+ description: string;
99
+ unit: string;
100
+ available_windows: string[];
101
+ contract_version?: string | null;
102
+ }
103
+ export interface StreamingSource {
104
+ path: string;
105
+ transport: string;
106
+ description: string;
107
+ filters?: Record<string, unknown>;
108
+ }
109
+ export interface AuditSource {
110
+ path: string;
111
+ description: string;
112
+ layers?: string[];
113
+ }
114
+ export interface CatalogResponse {
115
+ entities: Record<string, CatalogEntity>;
116
+ metrics: Record<string, CatalogMetric>;
117
+ streaming_sources?: Record<string, StreamingSource>;
118
+ audit_sources?: Record<string, AuditSource>;
119
+ }
120
+ export interface BatchItem {
121
+ id: string;
122
+ type: "entity" | "metric" | "query";
123
+ params: Record<string, unknown>;
124
+ }
125
+ export interface BatchResult {
126
+ id: string;
127
+ status: "ok" | "error";
128
+ data?: Record<string, unknown>;
129
+ error?: string;
130
+ }
131
+ export interface BatchResponse {
132
+ results: BatchResult[];
133
+ duration_ms: number;
134
+ }
135
+ export interface EventFilters {
136
+ eventType?: string;
137
+ entityId?: string;
138
+ signal?: AbortSignal;
139
+ }
140
+ export interface PipelineEvent {
141
+ event_id: string;
142
+ topic?: string | null;
143
+ processed_at?: string | null;
144
+ event_type?: string | null;
145
+ entity_id?: string | null;
146
+ latency_ms?: number | null;
147
+ [key: string]: unknown;
148
+ }
149
+ export interface ContractField {
150
+ name: string;
151
+ required?: boolean;
152
+ }
153
+ export interface ContractResponse {
154
+ entity: string;
155
+ version: string;
156
+ fields: ContractField[];
157
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ export declare const RETRYABLE_STATUS: Set<number>;
2
+ export interface RetryPolicyOptions {
3
+ maxAttempts?: number;
4
+ initialDelayMs?: number;
5
+ maxDelayMs?: number;
6
+ jitterFactor?: number;
7
+ }
8
+ export declare class RetryPolicy {
9
+ readonly maxAttempts: number;
10
+ readonly initialDelayMs: number;
11
+ readonly maxDelayMs: number;
12
+ readonly jitterFactor: number;
13
+ constructor(options?: RetryPolicyOptions);
14
+ computeDelay(attempt: number, retryAfterMs?: number): number;
15
+ }
16
+ export declare function isRetryableMethod(method: string, headers?: HeadersInit): boolean;
@@ -0,0 +1,45 @@
1
+ export const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
2
+ const DEFAULT_MAX_ATTEMPTS = 3;
3
+ const DEFAULT_INITIAL_DELAY_MS = 250;
4
+ const DEFAULT_MAX_DELAY_MS = 30_000;
5
+ const DEFAULT_JITTER_FACTOR = 0.3;
6
+ export class RetryPolicy {
7
+ maxAttempts;
8
+ initialDelayMs;
9
+ maxDelayMs;
10
+ jitterFactor;
11
+ constructor(options = {}) {
12
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
13
+ this.initialDelayMs = options.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
14
+ this.maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
15
+ this.jitterFactor = options.jitterFactor ?? DEFAULT_JITTER_FACTOR;
16
+ }
17
+ computeDelay(attempt, retryAfterMs) {
18
+ if (retryAfterMs != null) {
19
+ return Math.min(Math.max(retryAfterMs, 0), this.maxDelayMs);
20
+ }
21
+ const base = Math.min(this.initialDelayMs * 2 ** attempt, this.maxDelayMs);
22
+ if (this.jitterFactor === 0) {
23
+ return base;
24
+ }
25
+ const jitterRange = base * this.jitterFactor;
26
+ const jitter = (Math.random() * jitterRange * 2) - jitterRange;
27
+ return Math.max(0, Math.min(base + jitter, this.maxDelayMs));
28
+ }
29
+ }
30
+ export function isRetryableMethod(method, headers) {
31
+ const normalizedMethod = method.toUpperCase();
32
+ if (["GET", "HEAD", "PUT", "DELETE", "OPTIONS"].includes(normalizedMethod)) {
33
+ return true;
34
+ }
35
+ if (normalizedMethod !== "POST" || headers == null) {
36
+ return false;
37
+ }
38
+ if (headers instanceof Headers) {
39
+ return headers.has("Idempotency-Key");
40
+ }
41
+ if (Array.isArray(headers)) {
42
+ return headers.some(([key]) => key.toLowerCase() === "idempotency-key");
43
+ }
44
+ return Object.keys(headers).some((key) => key.toLowerCase() === "idempotency-key");
45
+ }
@@ -0,0 +1 @@
1
+ export declare function streamSseJson<T>(response: Response, signal?: AbortSignal): AsyncGenerator<T>;
@@ -0,0 +1,61 @@
1
+ import { AgentFlowError } from "./exceptions.js";
2
+ export async function* streamSseJson(response, signal) {
3
+ if (!response.body) {
4
+ throw new AgentFlowError("SSE response body is empty");
5
+ }
6
+ const reader = response.body.getReader();
7
+ const decoder = new TextDecoder();
8
+ let buffer = "";
9
+ try {
10
+ while (true) {
11
+ if (signal?.aborted) {
12
+ await reader.cancel();
13
+ return;
14
+ }
15
+ const chunk = await reader.read();
16
+ if (chunk.done) {
17
+ break;
18
+ }
19
+ buffer += decoder.decode(chunk.value, { stream: true }).replace(/\r\n/g, "\n");
20
+ for (;;) {
21
+ const boundaryIndex = buffer.indexOf("\n\n");
22
+ if (boundaryIndex === -1) {
23
+ break;
24
+ }
25
+ const frame = buffer.slice(0, boundaryIndex);
26
+ buffer = buffer.slice(boundaryIndex + 2);
27
+ const data = frame
28
+ .split("\n")
29
+ .filter((line) => line.startsWith("data:"))
30
+ .map((line) => line.slice(5).trimStart())
31
+ .join("\n");
32
+ if (data) {
33
+ yield JSON.parse(data);
34
+ }
35
+ }
36
+ }
37
+ buffer += decoder.decode().replace(/\r\n/g, "\n");
38
+ if (buffer.trim().startsWith("data:")) {
39
+ const data = buffer
40
+ .split("\n")
41
+ .filter((line) => line.startsWith("data:"))
42
+ .map((line) => line.slice(5).trimStart())
43
+ .join("\n");
44
+ if (data) {
45
+ yield JSON.parse(data);
46
+ }
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (error instanceof AgentFlowError) {
51
+ throw error;
52
+ }
53
+ if (error instanceof Error) {
54
+ throw new AgentFlowError(`Failed to read SSE stream: ${error.message}`);
55
+ }
56
+ throw new AgentFlowError("Failed to read SSE stream");
57
+ }
58
+ finally {
59
+ reader.releaseLock();
60
+ }
61
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@yuliaedomskikh/agentflow-client",
3
+ "version": "1.1.0",
4
+ "description": "TypeScript SDK for the AgentFlow API",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc --noEmit",
25
+ "test:unit": "vitest run --root .. --exclude=mutants/** tests/client.test.ts sdk-ts/tests/retry.test.ts sdk-ts/tests/circuitBreaker.test.ts sdk-ts/tests/resilience-integration.test.ts",
26
+ "test": "vitest run --root .. --exclude=mutants/** tests/client.test.ts sdk-ts/tests/retry.test.ts sdk-ts/tests/circuitBreaker.test.ts sdk-ts/tests/resilience-integration.test.ts"
27
+ },
28
+ "keywords": [
29
+ "agentflow",
30
+ "sdk",
31
+ "typescript",
32
+ "api"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/brownjuly2003-code/agentflow.git",
38
+ "directory": "sdk-ts"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^3.2.4"
46
+ }
47
+ }