10xgraph-client 0.6.0 → 1.0.0-rc.2

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 (149) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +11 -5
  3. package/dist/agent.d.ts +17 -5
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/cjs/agent.d.ts +33 -0
  6. package/dist/cjs/client.d.ts +533 -0
  7. package/dist/cjs/compat.d.ts +27 -0
  8. package/dist/cjs/endpoints/addThreadMessages.d.ts +25 -0
  9. package/dist/cjs/endpoints/clearThreadState.d.ts +14 -0
  10. package/dist/cjs/endpoints/deleteFile.d.ts +13 -0
  11. package/dist/cjs/endpoints/deleteMemory.d.ts +18 -0
  12. package/dist/cjs/endpoints/deleteThread.d.ts +18 -0
  13. package/dist/cjs/endpoints/deleteThreadMessage.d.ts +19 -0
  14. package/dist/cjs/endpoints/files.d.ts +115 -0
  15. package/dist/cjs/endpoints/fixGraph.d.ts +33 -0
  16. package/dist/cjs/endpoints/forgetMemories.d.ts +21 -0
  17. package/dist/cjs/endpoints/getMemory.d.ts +18 -0
  18. package/dist/cjs/endpoints/graph.d.ts +50 -0
  19. package/dist/cjs/endpoints/graphTools.d.ts +31 -0
  20. package/dist/cjs/endpoints/invoke.d.ts +66 -0
  21. package/dist/cjs/endpoints/listMemories.d.ts +24 -0
  22. package/dist/cjs/endpoints/metadata.d.ts +5 -0
  23. package/dist/cjs/endpoints/observability.d.ts +111 -0
  24. package/dist/cjs/endpoints/ping.d.ts +9 -0
  25. package/dist/cjs/endpoints/realtime.d.ts +154 -0
  26. package/dist/cjs/endpoints/searchMemory.d.ts +56 -0
  27. package/dist/cjs/endpoints/stateSchema.d.ts +49 -0
  28. package/dist/cjs/endpoints/stopGraph.d.ts +35 -0
  29. package/dist/cjs/endpoints/storeMemory.d.ts +32 -0
  30. package/dist/cjs/endpoints/stream.d.ts +67 -0
  31. package/dist/cjs/endpoints/threadDetails.d.ts +18 -0
  32. package/dist/cjs/endpoints/threadMessage.d.ts +16 -0
  33. package/dist/cjs/endpoints/threadMessages.d.ts +28 -0
  34. package/dist/cjs/endpoints/threadState.d.ts +13 -0
  35. package/dist/cjs/endpoints/threads.d.ts +35 -0
  36. package/dist/cjs/endpoints/updateMemory.d.ts +20 -0
  37. package/dist/cjs/endpoints/updateThreadMessageFeedback.d.ts +19 -0
  38. package/dist/cjs/endpoints/updateThreadState.d.ts +22 -0
  39. package/dist/cjs/endpoints/wsStream.d.ts +34 -0
  40. package/dist/cjs/errors.d.ts +212 -0
  41. package/dist/cjs/index.d.ts +41 -0
  42. package/dist/cjs/interrupt.d.ts +45 -0
  43. package/dist/cjs/message.d.ts +173 -0
  44. package/dist/cjs/package.json +1 -0
  45. package/dist/cjs/request.d.ts +60 -0
  46. package/dist/cjs/tools.d.ts +80 -0
  47. package/dist/cjs/ws.d.ts +47 -0
  48. package/dist/client.d.ts +95 -40
  49. package/dist/client.d.ts.map +1 -1
  50. package/dist/endpoints/addThreadMessages.d.ts +4 -0
  51. package/dist/endpoints/addThreadMessages.d.ts.map +1 -1
  52. package/dist/endpoints/clearThreadState.d.ts.map +1 -1
  53. package/dist/endpoints/deleteFile.d.ts +14 -0
  54. package/dist/endpoints/deleteFile.d.ts.map +1 -0
  55. package/dist/endpoints/deleteMemory.d.ts.map +1 -1
  56. package/dist/endpoints/deleteThread.d.ts.map +1 -1
  57. package/dist/endpoints/deleteThreadMessage.d.ts.map +1 -1
  58. package/dist/endpoints/files.d.ts +33 -0
  59. package/dist/endpoints/files.d.ts.map +1 -1
  60. package/dist/endpoints/fixGraph.d.ts +8 -3
  61. package/dist/endpoints/fixGraph.d.ts.map +1 -1
  62. package/dist/endpoints/forgetMemories.d.ts.map +1 -1
  63. package/dist/endpoints/getMemory.d.ts.map +1 -1
  64. package/dist/endpoints/graph.d.ts +20 -8
  65. package/dist/endpoints/graph.d.ts.map +1 -1
  66. package/dist/endpoints/graphTools.d.ts.map +1 -1
  67. package/dist/endpoints/invoke.d.ts +8 -0
  68. package/dist/endpoints/invoke.d.ts.map +1 -1
  69. package/dist/endpoints/listMemories.d.ts +6 -0
  70. package/dist/endpoints/listMemories.d.ts.map +1 -1
  71. package/dist/endpoints/observability.d.ts +54 -0
  72. package/dist/endpoints/observability.d.ts.map +1 -1
  73. package/dist/endpoints/ping.d.ts.map +1 -1
  74. package/dist/endpoints/realtime.d.ts +25 -4
  75. package/dist/endpoints/realtime.d.ts.map +1 -1
  76. package/dist/endpoints/searchMemory.d.ts.map +1 -1
  77. package/dist/endpoints/stateSchema.d.ts.map +1 -1
  78. package/dist/endpoints/stopGraph.d.ts +13 -3
  79. package/dist/endpoints/stopGraph.d.ts.map +1 -1
  80. package/dist/endpoints/storeMemory.d.ts.map +1 -1
  81. package/dist/endpoints/stream.d.ts +8 -0
  82. package/dist/endpoints/stream.d.ts.map +1 -1
  83. package/dist/endpoints/threadDetails.d.ts.map +1 -1
  84. package/dist/endpoints/threadMessage.d.ts.map +1 -1
  85. package/dist/endpoints/threadMessages.d.ts +6 -0
  86. package/dist/endpoints/threadMessages.d.ts.map +1 -1
  87. package/dist/endpoints/threadState.d.ts.map +1 -1
  88. package/dist/endpoints/threads.d.ts +6 -0
  89. package/dist/endpoints/threads.d.ts.map +1 -1
  90. package/dist/endpoints/updateMemory.d.ts.map +1 -1
  91. package/dist/endpoints/updateThreadMessageFeedback.d.ts +20 -0
  92. package/dist/endpoints/updateThreadMessageFeedback.d.ts.map +1 -0
  93. package/dist/endpoints/updateThreadState.d.ts +6 -1
  94. package/dist/endpoints/updateThreadState.d.ts.map +1 -1
  95. package/dist/endpoints/wsStream.d.ts +2 -0
  96. package/dist/endpoints/wsStream.d.ts.map +1 -1
  97. package/dist/errors.d.ts +60 -2
  98. package/dist/errors.d.ts.map +1 -1
  99. package/dist/index.cjs +3 -3
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.d.ts +3 -0
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +1742 -1217
  104. package/dist/index.js.map +1 -1
  105. package/dist/interrupt.d.ts +46 -0
  106. package/dist/interrupt.d.ts.map +1 -0
  107. package/dist/request.d.ts +28 -0
  108. package/dist/request.d.ts.map +1 -1
  109. package/dist/tools.d.ts +14 -0
  110. package/dist/tools.d.ts.map +1 -1
  111. package/package.json +11 -6
  112. package/src/agent.ts +17 -4
  113. package/src/client.ts +248 -128
  114. package/src/endpoints/addThreadMessages.ts +15 -10
  115. package/src/endpoints/clearThreadState.ts +19 -20
  116. package/src/endpoints/deleteFile.ts +65 -0
  117. package/src/endpoints/deleteMemory.ts +9 -13
  118. package/src/endpoints/deleteThread.ts +9 -13
  119. package/src/endpoints/deleteThreadMessage.ts +9 -13
  120. package/src/endpoints/files.ts +164 -131
  121. package/src/endpoints/fixGraph.ts +15 -16
  122. package/src/endpoints/forgetMemories.ts +44 -60
  123. package/src/endpoints/getMemory.ts +9 -9
  124. package/src/endpoints/graph.ts +28 -21
  125. package/src/endpoints/graphTools.ts +7 -13
  126. package/src/endpoints/invoke.ts +105 -84
  127. package/src/endpoints/listMemories.ts +18 -13
  128. package/src/endpoints/observability.ts +77 -34
  129. package/src/endpoints/ping.ts +7 -13
  130. package/src/endpoints/realtime.ts +101 -39
  131. package/src/endpoints/searchMemory.ts +19 -13
  132. package/src/endpoints/stateSchema.ts +7 -13
  133. package/src/endpoints/stopGraph.ts +20 -16
  134. package/src/endpoints/storeMemory.ts +7 -8
  135. package/src/endpoints/stream.ts +179 -61
  136. package/src/endpoints/threadDetails.ts +9 -14
  137. package/src/endpoints/threadMessage.ts +9 -9
  138. package/src/endpoints/threadMessages.ts +15 -9
  139. package/src/endpoints/threadState.ts +19 -20
  140. package/src/endpoints/threads.ts +13 -12
  141. package/src/endpoints/updateMemory.ts +9 -9
  142. package/src/endpoints/updateThreadMessageFeedback.ts +80 -0
  143. package/src/endpoints/updateThreadState.ts +28 -22
  144. package/src/endpoints/wsStream.ts +167 -42
  145. package/src/errors.ts +332 -93
  146. package/src/index.ts +9 -0
  147. package/src/interrupt.ts +60 -0
  148. package/src/request.ts +134 -0
  149. package/src/tools.ts +95 -0
@@ -1,11 +1,23 @@
1
1
  import { Message } from '../message.js';
2
2
  import { AgentState } from '../agent.js';
3
3
  import { ToolExecutor } from '../tools.js';
4
- import { createErrorFromResponse } from '../errors.js';
5
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
4
+ import {
5
+ createErrorFromResponse,
6
+ NetworkError,
7
+ RequestTimeoutError,
8
+ StreamError,
9
+ } from '../errors.js';
10
+ import {
11
+ buildHeaders,
12
+ getRequestCredentials,
13
+ linkAbortSignal,
14
+ RequestContext,
15
+ } from '../request.js';
6
16
 
7
17
  export interface StreamContext extends RequestContext {
8
18
  toolExecutor?: ToolExecutor;
19
+ /** Caller-supplied signal; aborting it cancels the request, including the open stream. */
20
+ signal?: AbortSignal;
9
21
  }
10
22
 
11
23
  // Stream Request payload (same as Invoke)
@@ -14,6 +26,12 @@ export interface StreamRequest {
14
26
  initial_state?: Record<string, any>;
15
27
  config?: Record<string, any>;
16
28
  recursion_limit?: number;
29
+ /** What to do when another run is active on the same thread (server default: 'wait'). */
30
+ on_busy?: 'wait' | 'cancel';
31
+ /** Value that answers an open interrupt. Send together with `interrupt_id`. */
32
+ resume?: unknown;
33
+ /** Id of the interrupt being answered by `resume`. */
34
+ interrupt_id?: string;
17
35
  response_granularity?: 'full' | 'partial' | 'low';
18
36
  }
19
37
 
@@ -44,27 +62,60 @@ export interface StreamChunk {
44
62
  timestamp?: number;
45
63
  }
46
64
 
65
+ /** Hooks that let the caller run an idle timer only while it waits on the network. */
66
+ interface ReadHooks {
67
+ beforeRead: () => void;
68
+ afterRead: () => void;
69
+ }
70
+
71
+ const STREAM_ENDPOINT = '/v1/graph/stream';
72
+
47
73
  /**
48
74
  * Parse NDJSON stream and yield individual chunks immediately as they arrive
49
75
  * Handles streaming response body as async iterable
50
76
  * Supports both newline-separated and concatenated JSON objects
77
+ *
78
+ * Leaving the loop early (break, throw, return) cancels the reader so the connection closes.
79
+ * A body that is cut mid-chunk, or a line that is not JSON, throws StreamError instead of
80
+ * ending as if the run had succeeded.
51
81
  */
52
82
  async function* parseStreamChunks(
53
83
  reader: ReadableStreamDefaultReader<Uint8Array>,
54
- debug: boolean
84
+ debug: boolean,
85
+ hooks?: ReadHooks
55
86
  ): AsyncGenerator<StreamChunk, void, unknown> {
56
87
  const decoder = new TextDecoder();
57
88
  let buffer = '';
89
+ let completed = false;
58
90
 
59
91
  try {
60
92
  while (true) {
61
- const { done, value } = await reader.read();
93
+ let result: ReadableStreamReadResult<Uint8Array>;
94
+ hooks?.beforeRead();
95
+ try {
96
+ result = await reader.read();
97
+ } catch (error) {
98
+ if ((error as Error)?.name === 'AbortError') throw error;
99
+ throw new StreamError(
100
+ `Stream connection lost: ${(error as Error)?.message ?? String(error)}`,
101
+ 'STREAM_INTERRUPTED',
102
+ 'unknown',
103
+ STREAM_ENDPOINT,
104
+ 'POST',
105
+ error
106
+ );
107
+ } finally {
108
+ hooks?.afterRead();
109
+ }
110
+ const { done, value } = result;
62
111
 
63
112
  if (done) {
64
113
  // Process any remaining data in buffer
114
+ buffer += decoder.decode();
65
115
  if (buffer.trim()) {
66
116
  yield* processBuffer(buffer, debug, true);
67
117
  }
118
+ completed = true;
68
119
  break;
69
120
  }
70
121
 
@@ -72,14 +123,33 @@ async function* parseStreamChunks(
72
123
  buffer += decoder.decode(value, { stream: true });
73
124
 
74
125
  // Immediately process and yield all complete chunks
75
- const result = yield* processBuffer(buffer, debug, false);
76
- buffer = result.remaining;
126
+ const processed = yield* processBuffer(buffer, debug, false);
127
+ buffer = processed.remaining;
77
128
  }
78
129
  } finally {
130
+ if (!completed) {
131
+ // Early exit: tell the server side to stop sending so the connection is released.
132
+ try {
133
+ await reader.cancel();
134
+ } catch {
135
+ /* already errored or closed */
136
+ }
137
+ }
79
138
  reader.releaseLock();
80
139
  }
81
140
  }
82
141
 
142
+ function badChunk(text: string, cause: unknown): StreamError {
143
+ return new StreamError(
144
+ `Stream data could not be parsed (${text.slice(0, 100)}); the stream was cut or corrupted`,
145
+ 'STREAM_TRUNCATED',
146
+ 'unknown',
147
+ STREAM_ENDPOINT,
148
+ 'POST',
149
+ cause
150
+ );
151
+ }
152
+
83
153
  /**
84
154
  * Process buffer and yield all complete JSON chunks
85
155
  * Returns the remaining incomplete buffer
@@ -103,9 +173,7 @@ function* processBuffer(
103
173
  }
104
174
  yield chunk;
105
175
  } catch (error) {
106
- if (debug) {
107
- console.warn('TenxGraphClient: Failed to parse NDJSON line:', line.slice(0, 100), error);
108
- }
176
+ throw badChunk(line, error);
109
177
  }
110
178
  }
111
179
  }
@@ -122,14 +190,7 @@ function* processBuffer(
122
190
  yield chunk;
123
191
  buffer = extracted.remaining;
124
192
  } catch (error) {
125
- if (debug) {
126
- console.warn(
127
- 'TenxGraphClient: Failed to parse concatenated JSON:',
128
- extracted.json.slice(0, 100),
129
- error
130
- );
131
- }
132
- break;
193
+ throw badChunk(extracted.json, error);
133
194
  }
134
195
  } else {
135
196
  // Incomplete JSON, keep in buffer
@@ -143,13 +204,7 @@ function* processBuffer(
143
204
  yield chunk;
144
205
  buffer = '';
145
206
  } catch (error) {
146
- if (debug) {
147
- console.warn(
148
- 'TenxGraphClient: Failed to parse final buffer:',
149
- buffer.slice(0, 100),
150
- error
151
- );
152
- }
207
+ throw badChunk(buffer.trim(), error);
153
208
  }
154
209
  }
155
210
  break;
@@ -246,7 +301,10 @@ export async function* streamInvoke(
246
301
  context: StreamContext,
247
302
  request: StreamRequest
248
303
  ): AsyncGenerator<StreamChunk, void, unknown> {
249
- const recursion_limit = request.recursion_limit || 25;
304
+ const recursion_limit = request.recursion_limit ?? 25;
305
+ // At least one request is always sent; the limit caps tool round trips after that.
306
+ const maxRuns = Math.max(1, recursion_limit);
307
+ const timeoutMs = context.timeout > 0 ? context.timeout : 0;
250
308
 
251
309
  if (context.debug) {
252
310
  console.debug('TenxGraphClient: Starting stream invoke with recursion_limit:', recursion_limit);
@@ -256,15 +314,20 @@ export async function* streamInvoke(
256
314
  let currentMessages = request.messages;
257
315
  let iterations = 0;
258
316
  const initial_state = request.initial_state;
259
- const config = request.config;
317
+ // Copied so a server-assigned thread_id can be added without touching the caller's object
318
+ let config = request.config;
260
319
  const response_granularity = request.response_granularity;
320
+ const on_busy = request.on_busy;
321
+ const resume = request.resume;
322
+ const interrupt_id = request.interrupt_id;
261
323
 
262
324
  // Main recursion loop (same as invoke, but using streaming)
263
- while (iterations < recursion_limit) {
325
+ while (true) {
326
+ context.signal?.throwIfAborted();
264
327
  iterations++;
265
328
 
266
329
  if (context.debug) {
267
- console.debug(`TenxGraphClient: Stream iteration ${iterations}/${recursion_limit}`);
330
+ console.debug(`TenxGraphClient: Stream iteration ${iterations}/${maxRuns}`);
268
331
  }
269
332
 
270
333
  // Prepare request for this iteration
@@ -273,6 +336,9 @@ export async function* streamInvoke(
273
336
  initial_state: iterations === 1 ? initial_state : undefined,
274
337
  config,
275
338
  recursion_limit,
339
+ on_busy,
340
+ resume: iterations === 1 ? resume : undefined,
341
+ interrupt_id: iterations === 1 ? interrupt_id : undefined,
276
342
  response_granularity,
277
343
  };
278
344
 
@@ -283,30 +349,52 @@ export async function* streamInvoke(
283
349
  );
284
350
  }
285
351
 
286
- // Open the stream and yield all chunks
352
+ // Open the stream and yield all chunks. The timeout is an idle timeout: it runs while the
353
+ // client waits on the network (headers, then each body read) and is paused while the
354
+ // caller processes a chunk, so a stalled proxy is cut but a slow consumer is not.
287
355
  const controller = new AbortController();
288
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
356
+ let timer: ReturnType<typeof setTimeout> | undefined;
357
+ const disarm = (): void => {
358
+ if (timer !== undefined) clearTimeout(timer);
359
+ timer = undefined;
360
+ };
361
+ const arm = (): void => {
362
+ disarm();
363
+ if (timeoutMs > 0) timer = setTimeout(() => controller.abort(), timeoutMs);
364
+ };
365
+ const unlink = linkAbortSignal(controller, context.signal);
366
+ let finished = false;
367
+ let sawError = false;
289
368
 
290
369
  try {
291
- const response = await fetch(`${context.baseUrl}/v1/graph/stream`, {
292
- method: 'POST',
293
- headers: buildHeaders(context as RequestContext, {
294
- 'Content-Type': 'application/json',
295
- Accept: 'application/x-ndjson, application/json',
296
- }),
297
- ...getRequestCredentials(context as RequestContext),
298
- body: JSON.stringify(iterationRequest),
299
- signal: controller.signal,
300
- });
301
-
302
- clearTimeout(timeoutId);
370
+ arm();
371
+ let response: Response;
372
+ try {
373
+ response = await fetch(`${context.baseUrl}${STREAM_ENDPOINT}`, {
374
+ method: 'POST',
375
+ headers: buildHeaders(context as RequestContext, {
376
+ 'Content-Type': 'application/json',
377
+ Accept: 'application/x-ndjson, application/json',
378
+ }),
379
+ ...getRequestCredentials(context as RequestContext),
380
+ body: JSON.stringify(iterationRequest),
381
+ signal: controller.signal,
382
+ });
383
+ } catch (error) {
384
+ if (error instanceof TypeError) {
385
+ throw new NetworkError(`Network error: ${error.message}`, STREAM_ENDPOINT, 'POST', error);
386
+ }
387
+ throw error;
388
+ }
389
+ disarm();
303
390
 
304
391
  if (!response.ok) {
305
392
  console.warn(`TenxGraphClient: Stream failed with HTTP ${response.status}`);
393
+ arm();
306
394
  const error = await createErrorFromResponse(
307
395
  response,
308
396
  'Stream request failed',
309
- '/v1/stream',
397
+ STREAM_ENDPOINT,
310
398
  'POST'
311
399
  );
312
400
  throw error;
@@ -320,15 +408,31 @@ export async function* streamInvoke(
320
408
  const responseMessages: Message[] = [];
321
409
  const reader = response.body.getReader();
322
410
 
323
- for await (const chunk of parseStreamChunks(reader, context.debug)) {
411
+ for await (const chunk of parseStreamChunks(reader, context.debug, {
412
+ beforeRead: arm,
413
+ afterRead: disarm,
414
+ })) {
324
415
  // Yield every chunk to the caller
325
416
  yield chunk;
326
417
 
418
+ // The server assigns a thread id when the caller omits one; reuse it so the
419
+ // tool round-trip stays on the same thread. A caller-supplied id is never replaced.
420
+ const returnedThreadId: string | undefined = chunk.thread_id || chunk.metadata?.thread_id;
421
+ if (returnedThreadId && !config?.thread_id) {
422
+ config = { ...(config ?? {}), thread_id: returnedThreadId };
423
+ }
424
+
425
+ // An error chunk (including a shutdown cut with data.interrupted) ends the run.
426
+ if (chunk.event === StreamEventType.ERROR) {
427
+ sawError = true;
428
+ }
429
+
327
430
  // Also collect messages for tool execution check
328
431
  if (chunk.message) {
329
432
  responseMessages.push(chunk.message);
330
433
  }
331
434
  }
435
+ finished = true;
332
436
 
333
437
  if (context.debug) {
334
438
  console.info('TenxGraphClient: Stream iteration completed');
@@ -338,7 +442,24 @@ export async function* streamInvoke(
338
442
  // Check if there are remote tool calls to execute
339
443
  const hasToolCalls = hasRemoteToolCalls(responseMessages);
340
444
 
341
- if (hasToolCalls && context.toolExecutor) {
445
+ if (hasToolCalls && context.toolExecutor && !sawError) {
446
+ if (iterations >= maxRuns) {
447
+ // The limit stops the loop with the server thread still waiting for tool results.
448
+ // Say so in the stream instead of ending as if the run were complete.
449
+ if (context.debug) {
450
+ console.warn(`TenxGraphClient: Recursion limit of ${recursion_limit} reached`);
451
+ }
452
+ yield {
453
+ event: StreamEventType.ERROR,
454
+ data: {
455
+ recursion_limit_reached: true,
456
+ message: `Recursion limit of ${recursion_limit} reached with tool calls still pending`,
457
+ },
458
+ thread_id: config?.thread_id,
459
+ };
460
+ break;
461
+ }
462
+
342
463
  if (context.debug) {
343
464
  console.debug('TenxGraphClient: Found remote tool calls, executing...');
344
465
  }
@@ -363,22 +484,23 @@ export async function* streamInvoke(
363
484
  break;
364
485
  }
365
486
  } catch (error) {
366
- clearTimeout(timeoutId);
367
-
368
487
  if ((error as Error).name === 'AbortError') {
488
+ if (context.signal?.aborted) {
489
+ // Cancelled by the caller, not a timeout: surface the abort as is.
490
+ throw error;
491
+ }
369
492
  console.warn(`TenxGraphClient: Stream timeout after ${context.timeout}ms`);
370
- throw new Error(`Request timeout after ${context.timeout}ms`);
493
+ throw new RequestTimeoutError(context.timeout, STREAM_ENDPOINT, 'POST', error);
371
494
  }
372
495
 
373
496
  console.error('TenxGraphClient: Stream failed:', error);
374
497
  throw error;
375
- }
376
- }
377
-
378
- // Check if we hit the recursion limit
379
- if (iterations >= recursion_limit) {
380
- if (context.debug) {
381
- console.warn(`TenxGraphClient: Recursion limit of ${recursion_limit} reached`);
498
+ } finally {
499
+ disarm();
500
+ unlink();
501
+ // Early exit (the caller broke out of the loop, or an error): cancel the request so
502
+ // the connection closes and the server run releases its thread lock.
503
+ if (!finished) controller.abort();
382
504
  }
383
505
  }
384
506
 
@@ -388,7 +510,7 @@ export async function* streamInvoke(
388
510
  }
389
511
 
390
512
  /**
391
- * Clean content blocks by removing empty arrays and undefined values
513
+ * Clean content blocks by removing undefined values (empty arrays are kept)
392
514
  * Server requires array of content blocks, not simplified string
393
515
  */
394
516
  function cleanContent(content: any[]): any[] {
@@ -397,11 +519,7 @@ function cleanContent(content: any[]): any[] {
397
519
  const cleaned: any = {};
398
520
 
399
521
  for (const [key, value] of Object.entries(block)) {
400
- // Skip empty arrays
401
- if (Array.isArray(value) && value.length === 0) {
402
- continue;
403
- }
404
- // Skip undefined values
522
+ // Skip undefined values (empty arrays are real data, e.g. a tool result of [])
405
523
  if (value === undefined) {
406
524
  continue;
407
525
  }
@@ -1,6 +1,12 @@
1
1
  import { ResponseMetadata } from './metadata.js';
2
2
  import { createErrorFromResponse } from '../errors.js';
3
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
3
+ import {
4
+ buildHeaders,
5
+ getRequestCredentials,
6
+ RequestContext,
7
+ fetchWithTimeout,
8
+ encodePathSegment,
9
+ } from '../request.js';
4
10
 
5
11
  export interface ThreadDetailsContext extends RequestContext {}
6
12
 
@@ -28,23 +34,17 @@ export async function threadDetails(
28
34
  console.debug(`TenxGraphClient: Fetching thread details for thread ${threadId}`);
29
35
  }
30
36
 
31
- const controller = new AbortController();
32
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
37
+ const url = `${context.baseUrl}/v1/threads/${encodePathSegment(threadId)}`;
33
38
 
34
- const url = `${context.baseUrl}/v1/threads/${threadId}`;
35
-
36
- const response = await fetch(url, {
39
+ const response = await fetchWithTimeout(context, url, {
37
40
  method: 'GET',
38
41
  headers: buildHeaders(context as RequestContext, {
39
42
  'Content-Type': 'application/json',
40
43
  accept: 'application/json',
41
44
  }),
42
45
  ...getRequestCredentials(context as RequestContext),
43
- signal: controller.signal,
44
46
  });
45
47
 
46
- clearTimeout(timeoutId);
47
-
48
48
  if (!response.ok) {
49
49
  console.warn(`TenxGraphClient: Thread details fetch failed with HTTP ${response.status}`);
50
50
  const error = await createErrorFromResponse(
@@ -64,11 +64,6 @@ export async function threadDetails(
64
64
 
65
65
  return data;
66
66
  } catch (error) {
67
- if ((error as Error).name === 'AbortError') {
68
- console.warn(`TenxGraphClient: Thread details fetch timeout after ${context.timeout}ms`);
69
- throw new Error(`Request timeout after ${context.timeout}ms`);
70
- }
71
-
72
67
  if (context.debug) {
73
68
  console.debug('TenxGraphClient: Thread details fetch failed:', error);
74
69
  }
@@ -1,7 +1,13 @@
1
1
  import { ResponseMetadata } from './metadata.js';
2
2
  import { Message } from '../message.js';
3
3
  import { createErrorFromResponse } from '../errors.js';
4
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
4
+ import {
5
+ buildHeaders,
6
+ getRequestCredentials,
7
+ RequestContext,
8
+ fetchWithTimeout,
9
+ encodePathSegment,
10
+ } from '../request.js';
5
11
 
6
12
  export interface ThreadMessageContext extends RequestContext {}
7
13
 
@@ -33,22 +39,16 @@ export async function threadMessage(
33
39
  );
34
40
  }
35
41
 
36
- const controller = new AbortController();
37
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
42
+ const url = `${context.baseUrl}/v1/threads/${encodePathSegment(request.threadId)}/messages/${encodePathSegment(request.messageId)}`;
38
43
 
39
- const url = `${context.baseUrl}/v1/threads/${request.threadId}/messages/${request.messageId}`;
40
-
41
- const response = await fetch(url, {
44
+ const response = await fetchWithTimeout(context, url, {
42
45
  method: 'GET',
43
46
  headers: buildHeaders(context as RequestContext, {
44
47
  'Content-Type': 'application/json',
45
48
  }),
46
49
  ...getRequestCredentials(context as RequestContext),
47
- signal: controller.signal,
48
50
  });
49
51
 
50
- clearTimeout(timeoutId);
51
-
52
52
  if (!response.ok) {
53
53
  console.warn(`TenxGraphClient: Thread message fetch failed with HTTP ${response.status}`);
54
54
  const error = await createErrorFromResponse(
@@ -1,7 +1,13 @@
1
1
  import { ResponseMetadata } from './metadata.js';
2
2
  import { Message } from '../message.js';
3
3
  import { createErrorFromResponse } from '../errors.js';
4
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
4
+ import {
5
+ buildHeaders,
6
+ getRequestCredentials,
7
+ RequestContext,
8
+ fetchWithTimeout,
9
+ encodePathSegment,
10
+ } from '../request.js';
5
11
 
6
12
  export interface ThreadMessagesContext extends RequestContext {}
7
13
 
@@ -14,6 +20,12 @@ export interface ThreadMessagesRequest {
14
20
 
15
21
  export interface ThreadMessagesData {
16
22
  messages: Message[];
23
+ /** True when more messages exist after this page. Absent on servers older than 1.0.0. */
24
+ has_more?: boolean;
25
+ /** Offset the server applied. */
26
+ offset?: number;
27
+ /** Limit the server applied (after its default and maximum clamp). */
28
+ limit?: number;
17
29
  }
18
30
 
19
31
  // Backward compatibility alias
@@ -36,9 +48,6 @@ export async function threadMessages(
36
48
  console.debug('TenxGraphClient: Fetching thread messages for thread', request.threadId);
37
49
  }
38
50
 
39
- const controller = new AbortController();
40
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
41
-
42
51
  // Build query parameters
43
52
  const params = new URLSearchParams();
44
53
  if (request.search !== undefined) {
@@ -52,19 +61,16 @@ export async function threadMessages(
52
61
  }
53
62
 
54
63
  const queryString = params.toString();
55
- const url = `${context.baseUrl}/v1/threads/${request.threadId}/messages${queryString ? '?' + queryString : ''}`;
64
+ const url = `${context.baseUrl}/v1/threads/${encodePathSegment(request.threadId)}/messages${queryString ? '?' + queryString : ''}`;
56
65
 
57
- const response = await fetch(url, {
66
+ const response = await fetchWithTimeout(context, url, {
58
67
  method: 'GET',
59
68
  headers: buildHeaders(context as RequestContext, {
60
69
  'Content-Type': 'application/json',
61
70
  }),
62
71
  ...getRequestCredentials(context as RequestContext),
63
- signal: controller.signal,
64
72
  });
65
73
 
66
- clearTimeout(timeoutId);
67
-
68
74
  if (!response.ok) {
69
75
  console.warn(
70
76
  `TenxGraphClient: Checkpoint messages fetch failed with HTTP ${response.status}`
@@ -1,7 +1,13 @@
1
1
  import { ResponseMetadata } from './metadata.js';
2
2
  import { AgentState } from '../agent.js';
3
3
  import { createErrorFromResponse } from '../errors.js';
4
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
4
+ import {
5
+ buildHeaders,
6
+ getRequestCredentials,
7
+ RequestContext,
8
+ fetchWithTimeout,
9
+ encodePathSegment,
10
+ } from '../request.js';
5
11
 
6
12
  export interface ThreadStateContext extends RequestContext {}
7
13
 
@@ -23,20 +29,18 @@ export async function threadState(
23
29
  console.debug(`TenxGraphClient: Fetching thread state for thread ${threadId}`);
24
30
  }
25
31
 
26
- const controller = new AbortController();
27
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
28
-
29
- const response = await fetch(`${context.baseUrl}/v1/threads/${threadId}/state`, {
30
- method: 'GET',
31
- headers: buildHeaders(context as RequestContext, {
32
- 'Content-Type': 'application/json',
33
- accept: 'application/json',
34
- }),
35
- ...getRequestCredentials(context as RequestContext),
36
- signal: controller.signal,
37
- });
38
-
39
- clearTimeout(timeoutId);
32
+ const response = await fetchWithTimeout(
33
+ context,
34
+ `${context.baseUrl}/v1/threads/${encodePathSegment(threadId)}/state`,
35
+ {
36
+ method: 'GET',
37
+ headers: buildHeaders(context as RequestContext, {
38
+ 'Content-Type': 'application/json',
39
+ accept: 'application/json',
40
+ }),
41
+ ...getRequestCredentials(context as RequestContext),
42
+ }
43
+ );
40
44
 
41
45
  if (!response.ok) {
42
46
  console.warn(`TenxGraphClient: Thread state fetch failed with HTTP ${response.status}`);
@@ -60,11 +64,6 @@ export async function threadState(
60
64
 
61
65
  return data;
62
66
  } catch (error) {
63
- if ((error as Error).name === 'AbortError') {
64
- console.warn(`TenxGraphClient: Thread state fetch timeout after ${context.timeout}ms`);
65
- throw new Error(`Request timeout after ${context.timeout}ms`);
66
- }
67
-
68
67
  if (context.debug) {
69
68
  console.debug(`TenxGraphClient: Thread state fetch failed:`, error);
70
69
  }
@@ -1,6 +1,11 @@
1
1
  import { ResponseMetadata } from './metadata.js';
2
2
  import { createErrorFromResponse } from '../errors.js';
3
- import { buildHeaders, getRequestCredentials, RequestContext } from '../request.js';
3
+ import {
4
+ buildHeaders,
5
+ getRequestCredentials,
6
+ RequestContext,
7
+ fetchWithTimeout,
8
+ } from '../request.js';
4
9
 
5
10
  export interface ThreadsContext extends RequestContext {}
6
11
 
@@ -21,6 +26,12 @@ export interface ThreadItem {
21
26
 
22
27
  export interface ThreadsData {
23
28
  threads: ThreadItem[];
29
+ /** True when more threads exist after this page. Absent on servers older than 1.0.0. */
30
+ has_more?: boolean;
31
+ /** Offset the server applied. */
32
+ offset?: number;
33
+ /** Limit the server applied (after its default and maximum clamp). */
34
+ limit?: number;
24
35
  }
25
36
 
26
37
  export interface ThreadsResponse {
@@ -50,9 +61,6 @@ export async function threads(
50
61
  }
51
62
  }
52
63
 
53
- const controller = new AbortController();
54
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
55
-
56
64
  // Build query parameters
57
65
  const params = new URLSearchParams();
58
66
  if (request?.search) {
@@ -68,18 +76,15 @@ export async function threads(
68
76
  const queryString = params.toString();
69
77
  const url = `${context.baseUrl}/v1/threads${queryString ? `?${queryString}` : ''}`;
70
78
 
71
- const response = await fetch(url, {
79
+ const response = await fetchWithTimeout(context, url, {
72
80
  method: 'GET',
73
81
  headers: buildHeaders(context as RequestContext, {
74
82
  'Content-Type': 'application/json',
75
83
  accept: 'application/json',
76
84
  }),
77
85
  ...getRequestCredentials(context as RequestContext),
78
- signal: controller.signal,
79
86
  });
80
87
 
81
- clearTimeout(timeoutId);
82
-
83
88
  if (!response.ok) {
84
89
  console.warn(`TenxGraphClient: Threads list fetch failed with HTTP ${response.status}`);
85
90
  const error = await createErrorFromResponse(
@@ -100,10 +105,6 @@ export async function threads(
100
105
 
101
106
  return data;
102
107
  } catch (error) {
103
- if ((error as Error).name === 'AbortError') {
104
- console.warn(`TenxGraphClient: Threads list fetch timeout after ${context.timeout}ms`);
105
- throw new Error(`Request timeout after ${context.timeout}ms`);
106
- }
107
108
  if (context.debug) {
108
109
  console.debug('TenxGraphClient: Threads list fetch failed:', error);
109
110
  }