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,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 UpdateMemoryContext extends RequestContext {}
6
12
 
@@ -31,10 +37,7 @@ export async function updateMemory(
31
37
  console.debug('TenxGraphClient: Updating memory with ID:', request.memoryId);
32
38
  }
33
39
 
34
- const controller = new AbortController();
35
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
36
-
37
- const url = `${context.baseUrl}/v1/store/memories/${request.memoryId}`;
40
+ const url = `${context.baseUrl}/v1/store/memories/${encodePathSegment(request.memoryId)}`;
38
41
 
39
42
  // Prepare request body
40
43
  const body = {
@@ -48,18 +51,15 @@ export async function updateMemory(
48
51
  console.debug('TenxGraphClient: Request payload:', JSON.stringify(body, null, 2));
49
52
  }
50
53
 
51
- const response = await fetch(url, {
54
+ const response = await fetchWithTimeout(context, url, {
52
55
  method: 'PUT',
53
56
  headers: buildHeaders(context as RequestContext, {
54
57
  'Content-Type': 'application/json',
55
58
  }),
56
59
  ...getRequestCredentials(context as RequestContext),
57
60
  body: JSON.stringify(body),
58
- signal: controller.signal,
59
61
  });
60
62
 
61
- clearTimeout(timeoutId);
62
-
63
63
  if (!response.ok) {
64
64
  console.warn(`TenxGraphClient: Update memory failed with HTTP ${response.status}`);
65
65
  const error = await createErrorFromResponse(
@@ -0,0 +1,80 @@
1
+ import { ResponseMetadata } from './metadata.js';
2
+ import { ThreadMessageData } from './threadMessage.js';
3
+ import { createErrorFromResponse } from '../errors.js';
4
+ import {
5
+ buildHeaders,
6
+ getRequestCredentials,
7
+ RequestContext,
8
+ fetchWithTimeout,
9
+ encodePathSegment,
10
+ } from '../request.js';
11
+
12
+ export interface UpdateThreadMessageFeedbackContext extends RequestContext {}
13
+
14
+ export interface UpdateThreadMessageFeedbackRequest {
15
+ threadId: string | number;
16
+ messageId: string;
17
+ /**
18
+ * Any JSON object. Replaces `message.metadata.feedback`. The server rejects
19
+ * feedback larger than 8192 serialized bytes or nested deeper than 10 levels (HTTP 422).
20
+ */
21
+ feedback: Record<string, unknown>;
22
+ }
23
+
24
+ export interface UpdateThreadMessageFeedbackResponse {
25
+ data: ThreadMessageData;
26
+ metadata: ResponseMetadata;
27
+ }
28
+
29
+ export async function updateThreadMessageFeedback(
30
+ context: UpdateThreadMessageFeedbackContext,
31
+ request: UpdateThreadMessageFeedbackRequest
32
+ ): Promise<UpdateThreadMessageFeedbackResponse> {
33
+ try {
34
+ if (context.debug) {
35
+ console.debug(
36
+ 'TenxGraphClient: Updating thread message feedback',
37
+ `thread: ${request.threadId}`,
38
+ `message: ${request.messageId}`
39
+ );
40
+ }
41
+
42
+ const url = `${context.baseUrl}/v1/threads/${encodePathSegment(request.threadId)}/messages/${encodePathSegment(request.messageId)}`;
43
+
44
+ const response = await fetchWithTimeout(context, url, {
45
+ method: 'PUT',
46
+ headers: buildHeaders(context as RequestContext, {
47
+ 'Content-Type': 'application/json',
48
+ accept: 'application/json',
49
+ }),
50
+ ...getRequestCredentials(context as RequestContext),
51
+ body: JSON.stringify({ feedback: request.feedback }),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ console.warn(
56
+ `TenxGraphClient: Update thread message feedback failed with HTTP ${response.status}`
57
+ );
58
+ const error = await createErrorFromResponse(
59
+ response,
60
+ 'Update thread message feedback failed',
61
+ '/v1/threads/{thread_id}/messages/{message_id}',
62
+ 'PUT'
63
+ );
64
+ throw error;
65
+ }
66
+
67
+ const data: UpdateThreadMessageFeedbackResponse = await response.json();
68
+
69
+ if (context.debug) {
70
+ console.info('TenxGraphClient: Thread message feedback updated successfully', data);
71
+ }
72
+
73
+ return data;
74
+ } catch (error) {
75
+ if (context.debug) {
76
+ console.debug('TenxGraphClient: Update thread message feedback failed:', error);
77
+ }
78
+ throw error;
79
+ }
80
+ }
@@ -1,13 +1,26 @@
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 UpdateThreadStateContext extends RequestContext {}
7
13
 
14
+ /**
15
+ * State fields the server accepts on update. `context_summary` and `execution_meta` are
16
+ * rejected by the server, so they are not part of this type.
17
+ */
18
+ export type UpdatableThreadState = Partial<Omit<AgentState, 'context_summary' | 'execution_meta'>> &
19
+ Record<string, any>;
20
+
8
21
  export interface UpdateThreadStateRequest {
9
22
  config: Record<string, any>;
10
- state: AgentState;
23
+ state: UpdatableThreadState;
11
24
  }
12
25
 
13
26
  export interface UpdateThreadStateData {
@@ -30,21 +43,19 @@ export async function updateThreadState(
30
43
  console.debug(`TenxGraphClient: Request payload:`, JSON.stringify(request, null, 2));
31
44
  }
32
45
 
33
- const controller = new AbortController();
34
- const timeoutId = setTimeout(() => controller.abort(), context.timeout);
35
-
36
- const response = await fetch(`${context.baseUrl}/v1/threads/${threadId}/state`, {
37
- method: 'PUT',
38
- headers: buildHeaders(context as RequestContext, {
39
- 'Content-Type': 'application/json',
40
- accept: 'application/json',
41
- }),
42
- ...getRequestCredentials(context as RequestContext),
43
- body: JSON.stringify(request),
44
- signal: controller.signal,
45
- });
46
-
47
- clearTimeout(timeoutId);
46
+ const response = await fetchWithTimeout(
47
+ context,
48
+ `${context.baseUrl}/v1/threads/${encodePathSegment(threadId)}/state`,
49
+ {
50
+ method: 'PUT',
51
+ headers: buildHeaders(context as RequestContext, {
52
+ 'Content-Type': 'application/json',
53
+ accept: 'application/json',
54
+ }),
55
+ ...getRequestCredentials(context as RequestContext),
56
+ body: JSON.stringify(request),
57
+ }
58
+ );
48
59
 
49
60
  if (!response.ok) {
50
61
  console.warn(`TenxGraphClient: Thread state update failed with HTTP ${response.status}`);
@@ -68,11 +79,6 @@ export async function updateThreadState(
68
79
 
69
80
  return data;
70
81
  } catch (error) {
71
- if ((error as Error).name === 'AbortError') {
72
- console.warn(`TenxGraphClient: Thread state update timeout after ${context.timeout}ms`);
73
- throw new Error(`Request timeout after ${context.timeout}ms`);
74
- }
75
-
76
82
  if (context.debug) {
77
83
  console.debug(`TenxGraphClient: Thread state update failed:`, error);
78
84
  }
@@ -2,6 +2,7 @@ import { Message } from '../message.js';
2
2
  import { ToolExecutor } from '../tools.js';
3
3
  import { RequestContext } from '../request.js';
4
4
  import { buildWsUrl, openWebSocket } from '../ws.js';
5
+ import { RequestTimeoutError, StreamError, WebSocketError } from '../errors.js';
5
6
  import { StreamChunk, StreamEventType, StreamRequest, serializeMessage } from './stream.js';
6
7
 
7
8
  // ─────────────────────────────────────────────────────────────────────────────
@@ -10,6 +11,8 @@ import { StreamChunk, StreamEventType, StreamRequest, serializeMessage } from '.
10
11
 
11
12
  export interface WsStreamContext extends RequestContext {
12
13
  toolExecutor?: ToolExecutor;
14
+ /** Caller-supplied signal; aborting it closes the socket and ends the stream. */
15
+ signal?: AbortSignal;
13
16
  }
14
17
 
15
18
  // ─────────────────────────────────────────────────────────────────────────────
@@ -23,9 +26,17 @@ interface WsGraphInput {
23
26
  initial_state?: Record<string, any>;
24
27
  config?: Record<string, any>;
25
28
  recursion_limit?: number;
29
+ on_busy?: 'wait' | 'cancel';
30
+ resume?: unknown;
31
+ interrupt_id?: string;
26
32
  response_granularity?: string;
27
33
  }
28
34
 
35
+ const WS_ENDPOINT = '/v1/graph/ws';
36
+
37
+ /** What the socket listeners hand to the generator. */
38
+ type QueueItem = StreamChunk | Error | { closed: true; code: number };
39
+
29
40
  // ─────────────────────────────────────────────────────────────────────────────
30
41
  // Helpers
31
42
  // ─────────────────────────────────────────────────────────────────────────────
@@ -70,21 +81,28 @@ export async function* wsStreamInvoke(
70
81
  context: WsStreamContext,
71
82
  request: StreamRequest
72
83
  ): AsyncGenerator<StreamChunk, void, unknown> {
73
- const url = buildWsUrl(context, '/v1/graph/ws');
84
+ context.signal?.throwIfAborted();
85
+ const url = buildWsUrl(context, WS_ENDPOINT);
74
86
 
75
87
  if (context.debug) {
76
88
  console.debug('TenxGraphClient [WS]: connecting to', url);
77
89
  }
78
90
 
79
91
  const ws = openWebSocket(url, context);
92
+ const recursion_limit = request.recursion_limit ?? 25;
93
+ // At least one run is always sent; the limit caps tool resumes after that.
94
+ const maxRuns = Math.max(1, recursion_limit);
95
+ // The timeout is an idle timeout: it applies while waiting for the socket to open and
96
+ // while waiting for the next frame, never while the caller processes a chunk.
97
+ const idleMs = context.timeout > 0 ? context.timeout : 0;
80
98
 
81
99
  // ── Queue bridge ──────────────────────────────────────────────────────────
82
100
  // WebSocket is event-based; async generators are pull-based.
83
101
  // We buffer incoming items in a queue and wake up the generator each time.
84
- const queue: Array<StreamChunk | Error | 'close'> = [];
102
+ const queue: QueueItem[] = [];
85
103
  let wakeUp: (() => void) | null = null;
86
104
 
87
- const enqueue = (item: StreamChunk | Error | 'close'): void => {
105
+ const enqueue = (item: QueueItem): void => {
88
106
  queue.push(item);
89
107
  if (wakeUp) {
90
108
  const fn = wakeUp;
@@ -102,60 +120,140 @@ export async function* wsStreamInvoke(
102
120
  }
103
121
  });
104
122
 
123
+ /** Next queued item, or a timeout error item when nothing arrives within the idle window. */
124
+ const nextItem = async (): Promise<QueueItem> => {
125
+ let timer: ReturnType<typeof setTimeout> | undefined;
126
+ if (idleMs > 0) {
127
+ timer = setTimeout(() => enqueue(new RequestTimeoutError(idleMs, WS_ENDPOINT, 'WS')), idleMs);
128
+ }
129
+ try {
130
+ await waitForItem();
131
+ } finally {
132
+ if (timer !== undefined) clearTimeout(timer);
133
+ }
134
+ return queue.shift()!;
135
+ };
136
+
105
137
  ws.addEventListener('message', (event: MessageEvent) => {
106
138
  try {
107
139
  const chunk = JSON.parse(event.data as string) as StreamChunk;
108
140
  enqueue(chunk);
109
141
  } catch (e) {
110
- if (context.debug) {
111
- console.warn('TenxGraphClient [WS]: failed to parse chunk:', e);
112
- }
142
+ // A frame that is not JSON means the stream is corrupt; do not skip it silently.
143
+ enqueue(
144
+ new StreamError(
145
+ 'WebSocket frame could not be parsed as JSON',
146
+ 'STREAM_TRUNCATED',
147
+ 'unknown',
148
+ WS_ENDPOINT,
149
+ 'WS',
150
+ e
151
+ )
152
+ );
113
153
  }
114
154
  });
115
155
 
116
156
  ws.addEventListener('error', () => {
117
- enqueue(new Error('WebSocket error'));
157
+ enqueue(new WebSocketError('WebSocket error', undefined, WS_ENDPOINT));
118
158
  });
119
159
 
120
160
  ws.addEventListener('close', (event: CloseEvent) => {
121
161
  if (context.debug) {
122
162
  console.debug('TenxGraphClient [WS]: connection closed, code=', event.code);
123
163
  }
124
- enqueue('close');
164
+ enqueue({ closed: true, code: event.code });
125
165
  });
126
166
 
127
- // Wait for the connection to open (or fail)
128
- await new Promise<void>((resolve, reject) => {
129
- // Numeric readyState literals (OPEN===1) instead of WebSocket.OPEN: the
130
- // global WebSocket may be absent on Node < 21 when a custom impl is injected.
131
- if (ws.readyState === 1) {
132
- resolve();
133
- return;
167
+ const makeAbortError = (): Error => {
168
+ const err = new Error('The operation was aborted');
169
+ err.name = 'AbortError';
170
+ return err;
171
+ };
172
+ const onAbort = (): void => {
173
+ enqueue(makeAbortError());
174
+ if (ws.readyState === 1 || ws.readyState === 0) {
175
+ ws.close(1000);
134
176
  }
135
- ws.addEventListener('open', () => resolve(), { once: true });
136
- ws.addEventListener('error', () => reject(new Error('WebSocket connection failed')), {
137
- once: true,
138
- });
139
- });
177
+ };
178
+ context.signal?.addEventListener('abort', onAbort, { once: true });
140
179
 
141
- if (context.debug) {
142
- console.debug('TenxGraphClient [WS]: connected');
143
- }
180
+ try {
181
+ // Wait for the connection to open (or fail). Every listener added here is removed again,
182
+ // whichever way the wait ends.
183
+ await new Promise<void>((resolve, reject) => {
184
+ if (context.signal?.aborted) {
185
+ reject(makeAbortError());
186
+ return;
187
+ }
188
+ // Numeric readyState literals (OPEN===1) instead of WebSocket.OPEN: the
189
+ // global WebSocket may be absent on Node < 22 when a custom impl is injected.
190
+ if (ws.readyState === 1) {
191
+ resolve();
192
+ return;
193
+ }
194
+ let timer: ReturnType<typeof setTimeout> | undefined;
195
+ const cleanup = (): void => {
196
+ if (timer !== undefined) clearTimeout(timer);
197
+ ws.removeEventListener('open', onOpen);
198
+ ws.removeEventListener('error', onError);
199
+ ws.removeEventListener('close', onClose);
200
+ context.signal?.removeEventListener('abort', onConnectAbort);
201
+ };
202
+ const onOpen = (): void => {
203
+ cleanup();
204
+ resolve();
205
+ };
206
+ const onError = (): void => {
207
+ cleanup();
208
+ reject(new WebSocketError('WebSocket connection failed', undefined, WS_ENDPOINT));
209
+ };
210
+ const onClose = (e: Event): void => {
211
+ cleanup();
212
+ const code = (e as CloseEvent)?.code;
213
+ reject(
214
+ new WebSocketError(`WebSocket closed before it opened (code ${code})`, code, WS_ENDPOINT)
215
+ );
216
+ };
217
+ const onConnectAbort = (): void => {
218
+ cleanup();
219
+ reject(makeAbortError());
220
+ };
221
+ ws.addEventListener('open', onOpen);
222
+ ws.addEventListener('error', onError);
223
+ ws.addEventListener('close', onClose);
224
+ context.signal?.addEventListener('abort', onConnectAbort, { once: true });
225
+ if (idleMs > 0) {
226
+ timer = setTimeout(() => {
227
+ cleanup();
228
+ reject(new RequestTimeoutError(idleMs, WS_ENDPOINT, 'WS'));
229
+ }, idleMs);
230
+ }
231
+ });
232
+ // Anything queued while connecting (an error event that raced the open) is stale.
233
+ queue.length = 0;
144
234
 
145
- // ── Main loop ─────────────────────────────────────────────────────────────
146
- let threadId: string | undefined = (request.config as Record<string, any>)?.thread_id;
235
+ if (context.debug) {
236
+ console.debug('TenxGraphClient [WS]: connected');
237
+ }
147
238
 
148
- let nextPayload: WsGraphInput = {
149
- invoke_type: 'fresh',
150
- messages: request.messages,
151
- initial_state: request.initial_state,
152
- config: request.config,
153
- recursion_limit: request.recursion_limit ?? 25,
154
- response_granularity: request.response_granularity,
155
- };
239
+ // ── Main loop ───────────────────────────────────────────────────────────
240
+ let threadId: string | undefined = (request.config as Record<string, any>)?.thread_id;
241
+ let runs = 0;
242
+
243
+ let nextPayload: WsGraphInput = {
244
+ invoke_type: 'fresh',
245
+ messages: request.messages,
246
+ initial_state: request.initial_state,
247
+ config: request.config,
248
+ recursion_limit,
249
+ on_busy: request.on_busy,
250
+ resume: request.resume,
251
+ interrupt_id: request.interrupt_id,
252
+ response_granularity: request.response_granularity,
253
+ };
156
254
 
157
- try {
158
255
  outerLoop: while (true) {
256
+ runs++;
159
257
  if (context.debug) {
160
258
  console.debug(
161
259
  `TenxGraphClient [WS]: sending ${nextPayload.invoke_type} request, thread_id=${threadId ?? 'new'}`
@@ -168,12 +266,16 @@ export async function* wsStreamInvoke(
168
266
  const runMessages: Message[] = [];
169
267
 
170
268
  innerLoop: while (true) {
171
- await waitForItem();
172
- const item = queue.shift()!;
173
-
174
- if (item === 'close') {
175
- // Server closed the connection unexpectedly
176
- break outerLoop;
269
+ const item = await nextItem();
270
+
271
+ if ('closed' in item) {
272
+ // The server closed the socket before it finished the run (1012 restart,
273
+ // 1013 busy, 1008 policy, 1011 error ...). Never report that as success.
274
+ throw new WebSocketError(
275
+ `WebSocket closed before the run finished (code ${item.code})`,
276
+ item.code,
277
+ WS_ENDPOINT
278
+ );
177
279
  }
178
280
 
179
281
  if (item instanceof Error) {
@@ -202,6 +304,12 @@ export async function* wsStreamInvoke(
202
304
 
203
305
  // Yield every chunk to the caller (same as HTTP stream)
204
306
  yield chunk;
307
+
308
+ // The server sends no "done" after an error chunk (busy thread, rate limit, not
309
+ // authorized, shutdown cut with data.interrupted). The error ends the run.
310
+ if (chunk.event === StreamEventType.ERROR) {
311
+ break outerLoop;
312
+ }
205
313
  }
206
314
 
207
315
  if (context.debug) {
@@ -212,6 +320,21 @@ export async function* wsStreamInvoke(
212
320
 
213
321
  // ── Tool-call detection & resume ──────────────────────────────────
214
322
  if (hasRemoteToolCalls(runMessages) && context.toolExecutor) {
323
+ if (runs >= maxRuns) {
324
+ if (context.debug) {
325
+ console.warn(`TenxGraphClient [WS]: recursion limit of ${recursion_limit} reached`);
326
+ }
327
+ yield {
328
+ event: StreamEventType.ERROR,
329
+ data: {
330
+ recursion_limit_reached: true,
331
+ message: `Recursion limit of ${recursion_limit} reached with tool calls still pending`,
332
+ },
333
+ thread_id: threadId,
334
+ };
335
+ break outerLoop;
336
+ }
337
+
215
338
  if (context.debug) {
216
339
  console.debug('TenxGraphClient [WS]: remote tool calls detected, executing…');
217
340
  }
@@ -228,7 +351,8 @@ export async function* wsStreamInvoke(
228
351
  invoke_type: 'resume',
229
352
  tool_result: toolResults.map(serializeMessage),
230
353
  config: { ...(request.config ?? {}), thread_id: threadId },
231
- recursion_limit: request.recursion_limit ?? 25,
354
+ recursion_limit,
355
+ on_busy: request.on_busy,
232
356
  response_granularity: request.response_granularity,
233
357
  };
234
358
  // continue outerLoop → send resume request
@@ -238,8 +362,9 @@ export async function* wsStreamInvoke(
238
362
  }
239
363
  }
240
364
  } finally {
365
+ context.signal?.removeEventListener('abort', onAbort);
241
366
  // CONNECTING===0, OPEN===1 (numeric literals; the global WebSocket may be
242
- // absent on Node < 21 when a custom impl is injected).
367
+ // absent on Node < 22 when a custom impl is injected).
243
368
  if (ws.readyState === 1 || ws.readyState === 0) {
244
369
  ws.close(1000);
245
370
  }