@agent-os-sdk/client 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/client/config.d.ts +49 -0
  2. package/dist/client/config.d.ts.map +1 -0
  3. package/dist/client/config.js +62 -0
  4. package/dist/client/pagination.d.ts +105 -0
  5. package/dist/client/pagination.d.ts.map +1 -0
  6. package/dist/client/pagination.js +117 -0
  7. package/dist/client/raw.d.ts +65 -0
  8. package/dist/client/raw.d.ts.map +1 -1
  9. package/dist/client/raw.js +78 -17
  10. package/dist/client/retry.d.ts +37 -0
  11. package/dist/client/retry.d.ts.map +1 -0
  12. package/dist/client/retry.js +108 -0
  13. package/dist/client/timeout.d.ts +26 -0
  14. package/dist/client/timeout.d.ts.map +1 -0
  15. package/dist/client/timeout.js +51 -0
  16. package/dist/errors/factory.d.ts +20 -0
  17. package/dist/errors/factory.d.ts.map +1 -0
  18. package/dist/errors/factory.js +97 -0
  19. package/dist/errors/index.d.ts +210 -0
  20. package/dist/errors/index.d.ts.map +1 -0
  21. package/dist/errors/index.js +283 -0
  22. package/dist/index.d.ts +11 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +26 -0
  25. package/dist/modules/audit.d.ts +27 -4
  26. package/dist/modules/audit.d.ts.map +1 -1
  27. package/dist/modules/audit.js +58 -2
  28. package/dist/modules/checkpoints.d.ts +1 -1
  29. package/dist/modules/checkpoints.d.ts.map +1 -1
  30. package/dist/modules/info.d.ts +49 -0
  31. package/dist/modules/info.d.ts.map +1 -1
  32. package/dist/modules/info.js +66 -0
  33. package/dist/modules/runs.d.ts +103 -0
  34. package/dist/modules/runs.d.ts.map +1 -1
  35. package/dist/modules/runs.js +258 -0
  36. package/dist/modules/tenants.d.ts +4 -1
  37. package/dist/modules/tenants.d.ts.map +1 -1
  38. package/dist/modules/tenants.js +3 -0
  39. package/dist/modules/threads.d.ts +24 -0
  40. package/dist/modules/threads.d.ts.map +1 -1
  41. package/dist/modules/threads.js +48 -1
  42. package/dist/sse/client.d.ts.map +1 -1
  43. package/dist/sse/client.js +17 -5
  44. package/package.json +49 -50
  45. package/src/client/config.ts +100 -0
  46. package/src/client/pagination.ts +218 -0
  47. package/src/client/raw.ts +138 -17
  48. package/src/client/retry.ts +150 -0
  49. package/src/client/timeout.ts +59 -0
  50. package/src/errors/factory.ts +135 -0
  51. package/src/errors/index.ts +365 -0
  52. package/src/index.ts +72 -2
  53. package/src/modules/audit.ts +77 -6
  54. package/src/modules/checkpoints.ts +1 -1
  55. package/src/modules/info.ts +108 -0
  56. package/src/modules/runs.ts +333 -0
  57. package/src/modules/tenants.ts +5 -2
  58. package/src/modules/threads.ts +57 -1
  59. package/src/sse/client.ts +21 -5
@@ -103,16 +103,31 @@ export class RunsModule {
103
103
  * agent_id: "agent-uuid",
104
104
  * input: { message: "Hello" }
105
105
  * });
106
+ *
107
+ * // With idempotency (safe to retry)
108
+ * const { data } = await client.runs.create({
109
+ * agent_id: "agent-uuid",
110
+ * input: { message: "Hello" },
111
+ * idempotency_key: "my-unique-key"
112
+ * });
106
113
  * ```
107
114
  */
108
115
  async create(body: {
109
116
  agent_id: string;
110
117
  thread?: { thread_id?: string } | { new_thread: true };
111
118
  input?: unknown;
119
+ /** Idempotency key for safe retries. When set, duplicate requests with the same key return the original response. */
112
120
  idempotency_key?: string;
113
121
  }): Promise<APIResponse<CreateRunResponse>> {
122
+ // Send Idempotency-Key in HEADER (enterprise standard) + body (backend compat)
123
+ const headers: Record<string, string> = {};
124
+ if (body.idempotency_key) {
125
+ headers["Idempotency-Key"] = body.idempotency_key;
126
+ }
127
+
114
128
  return this.client.POST<CreateRunResponse>("/v1/api/runs", {
115
129
  body,
130
+ headers,
116
131
  });
117
132
  }
118
133
 
@@ -138,6 +153,59 @@ export class RunsModule {
138
153
  });
139
154
  }
140
155
 
156
+ /**
157
+ * Iterate through all runs with automatic pagination.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * // Stream all completed runs
162
+ * for await (const run of client.runs.iterate({ status: "completed" })) {
163
+ * console.log(run.run_id, run.status);
164
+ * }
165
+ *
166
+ * // With limit
167
+ * for await (const run of client.runs.iterate({ agent_id: "..." }, { maxItems: 100 })) {
168
+ * processRun(run);
169
+ * }
170
+ * ```
171
+ */
172
+ async *iterate(
173
+ filters?: {
174
+ thread_id?: string;
175
+ agent_id?: string;
176
+ status?: string;
177
+ },
178
+ options?: { pageSize?: number; maxItems?: number; signal?: AbortSignal }
179
+ ): AsyncGenerator<Run, void, unknown> {
180
+ const pageSize = options?.pageSize ?? 100;
181
+ const maxItems = options?.maxItems ?? Infinity;
182
+ let offset = 0;
183
+ let yielded = 0;
184
+ let hasMore = true;
185
+
186
+ while (hasMore && yielded < maxItems) {
187
+ if (options?.signal?.aborted) return;
188
+
189
+ const response = await this.list({
190
+ ...filters,
191
+ limit: Math.min(pageSize, maxItems - yielded),
192
+ offset,
193
+ });
194
+
195
+ if (response.error) throw response.error;
196
+ const data = response.data!;
197
+
198
+ for (const run of data.items) {
199
+ if (yielded >= maxItems) return;
200
+ yield run;
201
+ yielded++;
202
+ }
203
+
204
+ offset += data.items.length;
205
+ hasMore = offset < data.total && data.items.length > 0;
206
+ }
207
+ }
208
+
141
209
  // ======================== Execution ========================
142
210
 
143
211
  /**
@@ -376,4 +444,269 @@ export class RunsModule {
376
444
  });
377
445
  yield* parseSSE<RunStreamEvent>(response, { onOpen: options?.onOpen });
378
446
  }
447
+
448
+ // ======================== FOLLOW (Enterprise SSE) ========================
449
+
450
+ /**
451
+ * Follow a run's event stream with automatic reconnection and resume.
452
+ *
453
+ * This is the enterprise-grade streaming method that:
454
+ * - Reconnects automatically on connection drops
455
+ * - Resumes from last received event using Last-Event-ID
456
+ * - Never duplicates or loses events
457
+ * - Terminates cleanly on 'close' event
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * for await (const event of client.runs.follow(runId)) {
462
+ * if (event.type === "run_event") {
463
+ * console.log(event.payload);
464
+ * } else if (event.type === "close") {
465
+ * console.log("Run completed");
466
+ * }
467
+ * }
468
+ * ```
469
+ */
470
+ async *follow(runId: string, options?: FollowOptions): AsyncGenerator<FollowEvent, void, unknown> {
471
+ const signal = options?.signal;
472
+ const maxReconnects = options?.maxReconnects ?? 10;
473
+ const baseDelayMs = options?.baseDelayMs ?? 1000;
474
+ const maxDelayMs = options?.maxDelayMs ?? 30000;
475
+
476
+ // nextSeq = next seq we expect to receive (or 0 at start)
477
+ let nextSeq = options?.startSeq ?? 0;
478
+ let reconnectCount = 0;
479
+ let receivedTerminal = false;
480
+
481
+ options?.onConnect?.();
482
+
483
+ while (!receivedTerminal && reconnectCount <= maxReconnects) {
484
+ if (signal?.aborted) return;
485
+
486
+ try {
487
+ // Build headers - Last-Event-ID is the LAST seq we received (nextSeq - 1)
488
+ const headers: Record<string, string> = {};
489
+ if (nextSeq > 0) {
490
+ headers["Last-Event-ID"] = String(nextSeq - 1);
491
+ }
492
+
493
+ const response = await this.client.streamGet("/v1/api/runs/{runId}/stream", {
494
+ params: {
495
+ path: { runId },
496
+ query: nextSeq > 0 ? { seq: nextSeq } : undefined // Also send as query for backends that support it
497
+ },
498
+ headers,
499
+ });
500
+
501
+ // Check for auth errors - don't reconnect on these
502
+ if (response.status === 401 || response.status === 403) {
503
+ const errorEvent: FollowEvent = {
504
+ type: "error",
505
+ payload: { code: `HTTP_${response.status}`, message: response.statusText },
506
+ seq: -1,
507
+ timestamp: new Date().toISOString(),
508
+ };
509
+ yield errorEvent;
510
+ return; // Don't reconnect on auth errors
511
+ }
512
+
513
+ if (!response.ok) {
514
+ throw new Error(`SSE connection failed: ${response.status}`);
515
+ }
516
+
517
+ // Reset reconnect count on successful connection
518
+ if (reconnectCount > 0) {
519
+ options?.onReconnect?.(reconnectCount);
520
+ }
521
+ reconnectCount = 0;
522
+
523
+ // Parse SSE stream
524
+ for await (const rawEvent of parseSSE<unknown>(response)) {
525
+ if (signal?.aborted) return;
526
+
527
+ // Narrow to known event types
528
+ const event = narrowFollowEvent(rawEvent);
529
+ if (!event) continue; // Skip unknown event types
530
+
531
+ // Update seq tracking if event has seq
532
+ if (typeof event.seq === "number" && event.seq >= 0) {
533
+ nextSeq = event.seq + 1;
534
+ }
535
+
536
+ // Check for terminal events
537
+ if (event.type === "close") {
538
+ receivedTerminal = true;
539
+ yield event;
540
+ return;
541
+ }
542
+
543
+ if (event.type === "error") {
544
+ // Error event from server - yield but continue (might reconnect)
545
+ yield event;
546
+ // If it's a fatal error, break and try reconnect
547
+ break;
548
+ }
549
+
550
+ yield event;
551
+ }
552
+
553
+ // Stream ended without close event - this is unexpected EOF
554
+ // Reconnect unless we already received terminal
555
+ if (!receivedTerminal && !signal?.aborted) {
556
+ options?.onDisconnect?.("eof");
557
+ reconnectCount++;
558
+ if (reconnectCount <= maxReconnects) {
559
+ await sleep(calculateBackoff(reconnectCount, baseDelayMs, maxDelayMs));
560
+ }
561
+ }
562
+
563
+ } catch (err) {
564
+ if (signal?.aborted) return;
565
+
566
+ options?.onDisconnect?.("error");
567
+ reconnectCount++;
568
+
569
+ if (reconnectCount <= maxReconnects) {
570
+ await sleep(calculateBackoff(reconnectCount, baseDelayMs, maxDelayMs));
571
+ } else {
572
+ // Max reconnects exceeded - yield error and stop
573
+ yield {
574
+ type: "error",
575
+ payload: { code: "MAX_RECONNECTS", message: `Max reconnects (${maxReconnects}) exceeded` },
576
+ seq: -1,
577
+ timestamp: new Date().toISOString(),
578
+ };
579
+ return;
580
+ }
581
+ }
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Wait for a specific event that matches a predicate.
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * // Wait for run completion
591
+ * const event = await client.runs.waitFor(runId, (e) => e.type === "close", {
592
+ * timeoutMs: 60000
593
+ * });
594
+ *
595
+ * // Wait for specific node execution
596
+ * const nodeEvent = await client.runs.waitFor(runId, (e) =>
597
+ * e.type === "run_event" && e.payload?.node === "my_node"
598
+ * );
599
+ * ```
600
+ */
601
+ async waitFor(
602
+ runId: string,
603
+ predicate: (event: FollowEvent) => boolean,
604
+ options?: { timeoutMs?: number; signal?: AbortSignal; startSeq?: number }
605
+ ): Promise<FollowEvent> {
606
+ const timeoutMs = options?.timeoutMs ?? 300_000; // 5 min default
607
+ const controller = new AbortController();
608
+
609
+ // Combine parent signal with our timeout
610
+ const onAbort = () => controller.abort();
611
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
612
+
613
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
614
+
615
+ try {
616
+ for await (const event of this.follow(runId, {
617
+ signal: controller.signal,
618
+ startSeq: options?.startSeq,
619
+ })) {
620
+ if (predicate(event)) {
621
+ return event;
622
+ }
623
+
624
+ // If we got close without matching predicate, error
625
+ if (event.type === "close") {
626
+ throw new Error("Run completed without matching event");
627
+ }
628
+ }
629
+
630
+ // Stream ended without match or terminal
631
+ throw new Error("Stream ended without matching event");
632
+
633
+ } catch (err) {
634
+ if (controller.signal.aborted && !options?.signal?.aborted) {
635
+ // Our timeout triggered the abort
636
+ const { TimeoutError } = await import("../errors/index.js");
637
+ throw new TimeoutError(timeoutMs);
638
+ }
639
+ throw err;
640
+ } finally {
641
+ clearTimeout(timeoutId);
642
+ options?.signal?.removeEventListener("abort", onAbort);
643
+ }
644
+ }
645
+ }
646
+
647
+ // ============================================================================
648
+ // Follow Types & Helpers
649
+ // ============================================================================
650
+
651
+ /** Options for runs.follow() */
652
+ export interface FollowOptions {
653
+ /** Starting sequence number (default: 0 = from beginning) */
654
+ startSeq?: number;
655
+ /** AbortSignal for cancellation */
656
+ signal?: AbortSignal;
657
+ /** Max reconnection attempts (default: 10) */
658
+ maxReconnects?: number;
659
+ /** Base delay for backoff in ms (default: 1000) */
660
+ baseDelayMs?: number;
661
+ /** Max delay for backoff in ms (default: 30000) */
662
+ maxDelayMs?: number;
663
+ /** Called when initially connected */
664
+ onConnect?: () => void;
665
+ /** Called when reconnecting (with attempt number) */
666
+ onReconnect?: (attempt: number) => void;
667
+ /** Called when disconnected (with reason) */
668
+ onDisconnect?: (reason: "eof" | "error") => void;
669
+ }
670
+
671
+ /** Event emitted by runs.follow() */
672
+ export interface FollowEvent {
673
+ type: "run_event" | "heartbeat" | "close" | "error";
674
+ seq: number;
675
+ timestamp: string;
676
+ payload?: Record<string, unknown> | null;
677
+ /** For run_event: the node that emitted this event */
678
+ node?: string;
679
+ }
680
+
681
+ /** Narrow raw SSE event to FollowEvent */
682
+ function narrowFollowEvent(raw: SSEEvent<unknown>): FollowEvent | null {
683
+ const eventType = raw.event ?? "message";
684
+ const validTypes = ["run_event", "heartbeat", "close", "error"];
685
+
686
+ if (!validTypes.includes(eventType)) {
687
+ return null;
688
+ }
689
+
690
+ const data = raw.data as Record<string, unknown> | null;
691
+
692
+ return {
693
+ type: eventType as FollowEvent["type"],
694
+ seq: typeof data?.seq === "number" ? data.seq : (raw.id ? parseInt(raw.id, 10) : -1),
695
+ timestamp: typeof data?.timestamp === "string" ? data.timestamp : new Date().toISOString(),
696
+ payload: data,
697
+ node: typeof data?.node === "string" ? data.node : undefined,
698
+ };
699
+ }
700
+
701
+ /** Calculate exponential backoff with jitter */
702
+ function calculateBackoff(attempt: number, baseMs: number, maxMs: number): number {
703
+ const exponential = baseMs * Math.pow(2, attempt - 1);
704
+ const capped = Math.min(exponential, maxMs);
705
+ const jitter = capped * 0.2 * Math.random();
706
+ return Math.floor(capped + jitter);
707
+ }
708
+
709
+ /** Sleep utility */
710
+ function sleep(ms: number): Promise<void> {
711
+ return new Promise(resolve => setTimeout(resolve, ms));
379
712
  }
@@ -27,8 +27,11 @@ export class TenantsModule {
27
27
  /**
28
28
  * List all tenants the user has access to.
29
29
  */
30
- async list(): Promise<APIResponse<Tenant[]>> {
31
- return this.client.GET<Tenant[]>("/v1/api/tenants", {
30
+ /**
31
+ * List all tenants the user has access to.
32
+ */
33
+ async list(): Promise<APIResponse<TenantListResponse>> {
34
+ return this.client.GET<TenantListResponse>("/v1/api/tenants", {
32
35
  headers: this.headers(),
33
36
  });
34
37
  }
@@ -79,16 +79,29 @@ export class ThreadsModule {
79
79
  * @example
80
80
  * ```ts
81
81
  * const { data: thread } = await client.threads.create();
82
+ *
83
+ * // With idempotency (safe to retry)
84
+ * const { data: thread } = await client.threads.create({
85
+ * idempotency_key: "my-unique-key"
86
+ * });
82
87
  * ```
83
88
  */
84
89
  async create(body?: {
85
90
  channel?: string;
86
91
  external_conversation_id?: string;
87
92
  metadata?: Record<string, unknown>;
93
+ /** Idempotency key for safe retries. When set, duplicate requests with the same key return the original response. */
94
+ idempotency_key?: string;
88
95
  }): Promise<APIResponse<Thread>> {
96
+ // Send Idempotency-Key in HEADER (enterprise standard) + body (backend compat)
97
+ const headers: Record<string, string> = { ...this.headers() };
98
+ if (body?.idempotency_key) {
99
+ headers["Idempotency-Key"] = body.idempotency_key;
100
+ }
101
+
89
102
  return this.client.POST<Thread>("/v1/api/threads", {
90
103
  body: body ?? {},
91
- headers: this.headers(),
104
+ headers,
92
105
  });
93
106
  }
94
107
 
@@ -122,6 +135,49 @@ export class ThreadsModule {
122
135
  });
123
136
  }
124
137
 
138
+ /**
139
+ * Iterate through all threads with automatic pagination.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * for await (const thread of client.threads.iterate()) {
144
+ * console.log(thread.id);
145
+ * }
146
+ * ```
147
+ */
148
+ async *iterate(
149
+ filters?: { workspace_id?: string },
150
+ options?: { pageSize?: number; maxItems?: number; signal?: AbortSignal }
151
+ ): AsyncGenerator<Thread, void, unknown> {
152
+ const pageSize = options?.pageSize ?? 100;
153
+ const maxItems = options?.maxItems ?? Infinity;
154
+ let offset = 0;
155
+ let yielded = 0;
156
+ let hasMore = true;
157
+
158
+ while (hasMore && yielded < maxItems) {
159
+ if (options?.signal?.aborted) return;
160
+
161
+ const response = await this.list({
162
+ ...filters,
163
+ limit: Math.min(pageSize, maxItems - yielded),
164
+ offset,
165
+ });
166
+
167
+ if (response.error) throw response.error;
168
+ const data = response.data!;
169
+
170
+ for (const thread of data.items) {
171
+ if (yielded >= maxItems) return;
172
+ yield thread;
173
+ yielded++;
174
+ }
175
+
176
+ offset += data.items.length;
177
+ hasMore = offset < data.total && data.items.length > 0;
178
+ }
179
+ }
180
+
125
181
  /**
126
182
  * Delete a thread.
127
183
  */
package/src/sse/client.ts CHANGED
@@ -51,7 +51,10 @@ export async function* parseSSE<T>(
51
51
 
52
52
  const reader = response.body.getReader();
53
53
  const decoder = new TextDecoder();
54
+
54
55
  let buffer = "";
56
+ // State must be maintained across chunks
57
+ let currentEvent: Partial<SSEEvent<T>> = { event: "message" };
55
58
 
56
59
  try {
57
60
  while (true) {
@@ -60,16 +63,32 @@ export async function* parseSSE<T>(
60
63
 
61
64
  buffer += decoder.decode(value, { stream: true });
62
65
  const lines = buffer.split("\n");
63
- buffer = lines.pop() ?? "";
64
66
 
65
- let currentEvent: Partial<SSEEvent<T>> = { event: "message" };
67
+ // Keep the last partial line in the buffer
68
+ buffer = lines.pop() ?? "";
66
69
 
67
70
  for (const line of lines) {
71
+ const trimmed = line.trim();
72
+
73
+ if (trimmed === "") {
74
+ // Empty line triggers event dispatch if we have data
75
+ if (currentEvent.data !== undefined) {
76
+ yield currentEvent as SSEEvent<T>;
77
+ }
78
+ // Reset for next event
79
+ currentEvent = { event: "message" };
80
+ continue;
81
+ }
82
+
68
83
  if (line.startsWith("event:")) {
69
84
  currentEvent.event = line.slice(6).trim();
70
85
  } else if (line.startsWith("data:")) {
71
86
  const dataStr = line.slice(5).trim();
72
87
  try {
88
+ // Accumulate data if meaningful (though standard SSE usually has one data line per event,
89
+ // multiline data is possible but rare in this specific protocol.
90
+ // For simplicity and matching current backend, we overwrite or concatenation check could be stricter).
91
+ // Given strict JSON protocol, we assume valid info per data line or single data line.
73
92
  currentEvent.data = JSON.parse(dataStr) as T;
74
93
  } catch {
75
94
  currentEvent.data = dataStr as T;
@@ -78,9 +97,6 @@ export async function* parseSSE<T>(
78
97
  currentEvent.id = line.slice(3).trim();
79
98
  } else if (line.startsWith("retry:")) {
80
99
  currentEvent.retry = parseInt(line.slice(6).trim(), 10);
81
- } else if (line === "" && currentEvent.data !== undefined) {
82
- yield currentEvent as SSEEvent<T>;
83
- currentEvent = { event: "message" };
84
100
  }
85
101
  }
86
102
  }