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
package/src/request.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { WebSocketImpl } from './ws.js';
2
+ import { NetworkError, RequestTimeoutError, TenxGraphError } from './errors.js';
2
3
 
3
4
  export interface TenxGraphBearerAuth {
4
5
  type: 'bearer';
@@ -20,6 +21,30 @@ export interface TenxGraphHeaderAuth {
20
21
 
21
22
  export type TenxGraphAuth = TenxGraphBearerAuth | TenxGraphBasicAuth | TenxGraphHeaderAuth;
22
23
 
24
+ /**
25
+ * Abort `controller` when the caller's `signal` aborts. Returns a cleanup function.
26
+ * A signal that is already aborted aborts the controller immediately.
27
+ */
28
+ export function linkAbortSignal(
29
+ controller: AbortController,
30
+ signal?: AbortSignal | null
31
+ ): () => void {
32
+ if (!signal) return () => {};
33
+ if (signal.aborted) {
34
+ controller.abort();
35
+ return () => {};
36
+ }
37
+ const onAbort = (): void => controller.abort();
38
+ signal.addEventListener('abort', onAbort, { once: true });
39
+ return () => signal.removeEventListener('abort', onAbort);
40
+ }
41
+
42
+ /** Per-call options accepted by every client method that makes a request. */
43
+ export interface RequestOptions {
44
+ /** Abort to cancel the request. Rejects with the signal's AbortError, never as a timeout. */
45
+ signal?: AbortSignal;
46
+ }
47
+
23
48
  export interface RequestContext {
24
49
  baseUrl: string;
25
50
  authToken?: string | null;
@@ -29,6 +54,8 @@ export interface RequestContext {
29
54
  timeout: number;
30
55
  debug: boolean;
31
56
  webSocketImpl?: WebSocketImpl;
57
+ /** Caller's cancellation signal for this call. Set per call by the client, not stored. */
58
+ signal?: AbortSignal;
32
59
  }
33
60
 
34
61
  function setHeader(
@@ -166,3 +193,110 @@ export function headerAuth(
166
193
  ): TenxGraphHeaderAuth {
167
194
  return { type: 'header', name, value, prefix };
168
195
  }
196
+
197
+ /** Percent-encode one URL path segment (thread, message or memory id). */
198
+ export function encodePathSegment(value: string | number): string {
199
+ return encodeURIComponent(String(value));
200
+ }
201
+
202
+ /** Strip trailing slashes so `${baseUrl}/path` never produces `//path`. */
203
+ export function normalizeBaseUrl(baseUrl: string): string {
204
+ // A loop, not /\/+$/: that regex is quadratic on input with many slashes (CodeQL js/polynomial-redos).
205
+ let end = baseUrl.length;
206
+ while (end > 0 && baseUrl.charCodeAt(end - 1) === 47 /* '/' */) end--;
207
+ return baseUrl.slice(0, end);
208
+ }
209
+
210
+ const MAX_REDIRECTS = 5;
211
+
212
+ function originOf(url: string): string | null {
213
+ try {
214
+ return new URL(url).origin;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ function pathOf(url: string): string {
221
+ try {
222
+ return new URL(url).pathname;
223
+ } catch {
224
+ return url;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * fetch with the shared client policy:
230
+ * - the timeout timer is always cleared (also when fetch rejects) and does not keep a Node
231
+ * process alive;
232
+ * - `context.signal` cancels the call (rejects with the AbortError, never as a timeout);
233
+ * - a timeout throws RequestTimeoutError, any other fetch rejection throws NetworkError
234
+ * with the original error as `cause`;
235
+ * - with header-type credentials other than Authorization (e.g. X-API-Key, which fetch does not
236
+ * strip on a redirect)
237
+ * redirects are followed only within the same origin; a cross-origin redirect is refused.
238
+ */
239
+ export async function fetchWithTimeout(
240
+ context: Pick<RequestContext, 'timeout' | 'signal' | 'auth'>,
241
+ url: string,
242
+ init: RequestInit = {}
243
+ ): Promise<Response> {
244
+ const controller = new AbortController();
245
+ const timer =
246
+ context.timeout > 0 ? setTimeout(() => controller.abort(), context.timeout) : undefined;
247
+ (timer as unknown as { unref?: () => void } | undefined)?.unref?.();
248
+ const unlink = linkAbortSignal(controller, context.signal);
249
+ const method = (init.method || 'GET').toUpperCase();
250
+ // fetch strips the standard Authorization header on a cross-origin redirect (checked against
251
+ // Node's undici), but forwards any other custom header, so only those need the guard.
252
+ const guardRedirects =
253
+ context.auth?.type === 'header' && context.auth.name.toLowerCase() !== 'authorization';
254
+
255
+ try {
256
+ let currentUrl = url;
257
+ let currentInit: RequestInit = { ...init, signal: controller.signal };
258
+ if (guardRedirects) currentInit.redirect = 'manual';
259
+
260
+ for (let hops = 0; ; hops++) {
261
+ const response = await fetch(currentUrl, currentInit);
262
+ if (guardRedirects && response.type === 'opaqueredirect') {
263
+ // Browsers hide the Location of a manual redirect, so it cannot be followed or checked.
264
+ throw new NetworkError(
265
+ 'The server redirected the request and the browser does not expose the target, so it ' +
266
+ 'was not followed (header credentials must not be sent to another origin). ' +
267
+ 'Point baseUrl at the final URL.',
268
+ pathOf(url),
269
+ method
270
+ );
271
+ }
272
+ if (!guardRedirects || !(response.status >= 300 && response.status < 400)) {
273
+ return response;
274
+ }
275
+ const location = response.headers?.get('location');
276
+ const target = location ? new URL(location, currentUrl).toString() : null;
277
+ if (!target || originOf(target) !== originOf(url) || hops >= MAX_REDIRECTS) {
278
+ throw new NetworkError(
279
+ 'Refusing to follow a redirect that leaves the base URL origin (credentials would be forwarded)',
280
+ pathOf(url),
281
+ method
282
+ );
283
+ }
284
+ currentUrl = target;
285
+ if (response.status === 303) {
286
+ currentInit = { ...currentInit, method: 'GET', body: undefined };
287
+ }
288
+ }
289
+ } catch (error) {
290
+ if (error instanceof TenxGraphError) throw error;
291
+ const name = (error as { name?: string } | null)?.name;
292
+ if (name === 'AbortError') {
293
+ if (context.signal?.aborted) throw error;
294
+ throw new RequestTimeoutError(context.timeout, pathOf(url), method, error);
295
+ }
296
+ const detail = error instanceof Error ? error.message : String(error);
297
+ throw new NetworkError(`Network request failed: ${detail}`, pathOf(url), method, error);
298
+ } finally {
299
+ if (timer !== undefined) clearTimeout(timer);
300
+ unlink();
301
+ }
302
+ }
package/src/tools.ts CHANGED
@@ -52,6 +52,56 @@ export interface ToolRegistration {
52
52
  description?: string;
53
53
  parameters?: ToolParameter;
54
54
  handler: ToolHandler;
55
+ /**
56
+ * Runs on the server-sent arguments before the handler (after the declared `parameters`
57
+ * check). Throw, or return `false`, to reject the call: the model receives a failed tool
58
+ * result and the handler is not run.
59
+ */
60
+ validate?: (args: any) => boolean | void | Promise<boolean | void>;
61
+ /** Reject calls whose JSON-serialised arguments exceed this many bytes. Default 1 MiB. */
62
+ maxArgsBytes?: number;
63
+ }
64
+
65
+ /** Default cap on serialised tool-call arguments. */
66
+ export const DEFAULT_MAX_TOOL_ARGS_BYTES = 1024 * 1024;
67
+
68
+ /** Check `args` against the top level of a declared JSON Schema; returns an error message or null. */
69
+ export function checkToolArguments(args: unknown, parameters?: ToolParameter): string | null {
70
+ if (!parameters) return null;
71
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) {
72
+ return 'arguments must be an object';
73
+ }
74
+ const record = args as Record<string, unknown>;
75
+ for (const key of parameters.required ?? []) {
76
+ if (!(key in record)) return `missing required argument '${key}'`;
77
+ }
78
+ const properties = parameters.properties ?? {};
79
+ if (parameters.additionalProperties === false) {
80
+ for (const key of Object.keys(record)) {
81
+ if (!(key in properties)) return `unexpected argument '${key}'`;
82
+ }
83
+ }
84
+ for (const [key, schema] of Object.entries(properties)) {
85
+ const value = record[key];
86
+ const expected = (schema as { type?: unknown } | null)?.type;
87
+ if (value === undefined || typeof expected !== 'string') continue;
88
+ const ok =
89
+ expected === 'string'
90
+ ? typeof value === 'string'
91
+ : expected === 'number'
92
+ ? typeof value === 'number'
93
+ : expected === 'integer'
94
+ ? Number.isInteger(value)
95
+ : expected === 'boolean'
96
+ ? typeof value === 'boolean'
97
+ : expected === 'array'
98
+ ? Array.isArray(value)
99
+ : expected === 'object'
100
+ ? value !== null && typeof value === 'object' && !Array.isArray(value)
101
+ : true;
102
+ if (!ok) return `argument '${key}' must be of type ${expected}`;
103
+ }
104
+ return null;
55
105
  }
56
106
 
57
107
  // Internal tool definition (extends handler)
@@ -60,6 +110,8 @@ export interface ToolDefinition extends ToolHandler {
60
110
  description?: string;
61
111
  parameters?: ToolParameter;
62
112
  node?: string;
113
+ validate?: ToolRegistration['validate'];
114
+ maxArgsBytes?: number;
63
115
  }
64
116
 
65
117
  export class ToolExecutor {
@@ -108,6 +160,16 @@ export class ToolExecutor {
108
160
  writable: true,
109
161
  configurable: true,
110
162
  });
163
+ Object.defineProperty(toolDef, 'validate', {
164
+ value: registration.validate,
165
+ writable: true,
166
+ configurable: true,
167
+ });
168
+ Object.defineProperty(toolDef, 'maxArgsBytes', {
169
+ value: registration.maxArgsBytes,
170
+ writable: true,
171
+ configurable: true,
172
+ });
111
173
  Object.defineProperty(toolDef, 'node', {
112
174
  value: registration.node,
113
175
  writable: true,
@@ -174,14 +236,47 @@ export class ToolExecutor {
174
236
 
175
237
  const results: Message[] = [];
176
238
 
239
+ const seenCallIds = new Set<string>();
240
+ const failed = (callId: string, error: string): Message =>
241
+ Message.tool_message([
242
+ new ToolResultBlock({
243
+ call_id: callId,
244
+ output: { error },
245
+ status: 'failed',
246
+ is_error: true,
247
+ }),
248
+ ]);
249
+
177
250
  // Execute each tool call
178
251
  for (const call of toolCalls) {
179
252
  const toolName = call.name;
180
253
 
254
+ if (call.id && seenCallIds.has(call.id)) {
255
+ results.push(failed(call.id, `Duplicate tool call id '${call.id}' rejected`));
256
+ continue;
257
+ }
258
+ if (call.id) seenCallIds.add(call.id);
259
+
181
260
  if (this.tools.has(toolName)) {
182
261
  const tool = this.tools.get(toolName)!;
183
262
 
184
263
  try {
264
+ const maxBytes = tool.maxArgsBytes ?? DEFAULT_MAX_TOOL_ARGS_BYTES;
265
+ const size = JSON.stringify(call.args ?? null)?.length ?? 0;
266
+ if (size > maxBytes) {
267
+ results.push(failed(call.id, `Arguments for '${toolName}' exceed ${maxBytes} bytes`));
268
+ continue;
269
+ }
270
+ const invalid = checkToolArguments(call.args ?? {}, tool.parameters);
271
+ if (invalid) {
272
+ results.push(failed(call.id, `Invalid arguments for '${toolName}': ${invalid}`));
273
+ continue;
274
+ }
275
+ if (tool.validate && (await tool.validate(call.args)) === false) {
276
+ results.push(failed(call.id, `Arguments for '${toolName}' were rejected`));
277
+ continue;
278
+ }
279
+
185
280
  // Execute the tool
186
281
  const result = await tool(call.args);
187
282