@assistant-ui/react-devtools 1.2.16 → 1.2.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;cAIa,qBACX,gBACA,OAAA;cA+DW;;;;;;;;cAwCA,kBAAmB,gBAAgB;cAoBnC,oBAAqB;cAGrB,wBACX,SAAS,6BACR"}
1
+ {"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;cAKa,qBACX,gBACA,OAAA;cA8FW;;;;;;;;cAwCA,kBAAmB,gBAAgB;cAoBnC,oBAAqB;cAGrB,wBACX,SAAS,6BACR"}
@@ -1,3 +1,4 @@
1
+ import { UNSERIALIZABLE, readProperty } from "./unserializable.js";
1
2
  import { normalizeToolList } from "./toolNormalization.js";
2
3
  //#region src/utils/serialization.ts
3
4
  const sanitizeForMessage = (value, seen = /* @__PURE__ */ new WeakSet()) => {
@@ -5,36 +6,59 @@ const sanitizeForMessage = (value, seen = /* @__PURE__ */ new WeakSet()) => {
5
6
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
6
7
  if (typeof value === "bigint" || typeof value === "symbol") return String(value);
7
8
  if (typeof value === "function") return "[Function]";
8
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
9
- if (value instanceof Map) {
10
- if (seen.has(value)) return "[Circular]";
11
- seen.add(value);
12
- const result = {};
13
- for (const [key, entry] of value.entries()) result[String(key)] = sanitizeForMessage(entry, seen);
14
- seen.delete(value);
15
- return result;
16
- }
17
- if (value instanceof Set) {
18
- if (seen.has(value)) return "[Circular]";
19
- seen.add(value);
20
- const result = Array.from(value).map((entry) => sanitizeForMessage(entry, seen));
21
- seen.delete(value);
22
- return result;
23
- }
24
- if (Array.isArray(value)) {
25
- if (seen.has(value)) return "[Circular]";
26
- seen.add(value);
27
- const result = value.map((entry) => sanitizeForMessage(entry, seen)).filter((item) => item !== void 0);
28
- seen.delete(value);
29
- return result;
30
- }
31
9
  if (typeof value === "object") {
32
10
  if (seen.has(value)) return "[Circular]";
33
11
  seen.add(value);
34
- const result = {};
35
- for (const [key, entry] of Object.entries(value)) result[key] = sanitizeForMessage(entry, seen);
36
- seen.delete(value);
37
- return result;
12
+ try {
13
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
14
+ if (value instanceof Map) {
15
+ const result = {};
16
+ const nextSuffixByKey = /* @__PURE__ */ new Map();
17
+ for (const [key, entry] of value.entries()) {
18
+ let serializedKey;
19
+ try {
20
+ serializedKey = String(key);
21
+ } catch {
22
+ serializedKey = UNSERIALIZABLE;
23
+ }
24
+ if (Object.hasOwn(result, serializedKey)) {
25
+ const baseKey = serializedKey;
26
+ let suffix = nextSuffixByKey.get(baseKey) ?? 2;
27
+ do {
28
+ serializedKey = `${baseKey} (${suffix})`;
29
+ suffix += 1;
30
+ } while (Object.hasOwn(result, serializedKey));
31
+ nextSuffixByKey.set(baseKey, suffix);
32
+ } else nextSuffixByKey.set(serializedKey, 2);
33
+ result[serializedKey] = sanitizeForMessage(entry, seen);
34
+ }
35
+ return result;
36
+ }
37
+ if (value instanceof Set) return Array.from(value).map((entry) => sanitizeForMessage(entry, seen));
38
+ if (Array.isArray(value)) {
39
+ const result = [];
40
+ const length = value.length;
41
+ for (let index = 0; index < length; index++) try {
42
+ if (!(index in value)) continue;
43
+ const item = sanitizeForMessage(value[index], seen);
44
+ if (item !== void 0) result.push(item);
45
+ } catch {
46
+ result.push(UNSERIALIZABLE);
47
+ }
48
+ return result;
49
+ }
50
+ const result = {};
51
+ for (const key of Object.keys(value)) try {
52
+ result[key] = sanitizeForMessage(value[key], seen);
53
+ } catch {
54
+ result[key] = UNSERIALIZABLE;
55
+ }
56
+ return result;
57
+ } catch {
58
+ return UNSERIALIZABLE;
59
+ } finally {
60
+ seen.delete(value);
61
+ }
38
62
  }
39
63
  return value;
40
64
  };
@@ -91,9 +115,9 @@ const serializeModelContext = (context) => {
91
115
  if (!context || typeof context !== "object") return;
92
116
  const modelContext = context;
93
117
  const result = {};
94
- const systemValue = modelContext.system;
118
+ const systemValue = readProperty(modelContext, "system");
95
119
  if (typeof systemValue === "string" && systemValue.length > 0) result.system = systemValue;
96
- const tools = normalizeToolList(modelContext.tools);
120
+ const tools = normalizeToolList(readProperty(modelContext, "tools"));
97
121
  if (tools.length > 0) result.tools = tools.map((tool) => {
98
122
  return {
99
123
  ...tool,
@@ -104,12 +128,14 @@ const serializeModelContext = (context) => {
104
128
  ...tool.backendDefault !== void 0 ? { backendDefault: sanitizeForMessage(tool.backendDefault) } : {}
105
129
  };
106
130
  });
107
- if (modelContext.callSettings !== void 0) {
108
- const callSettings = sanitizeAndRedact(modelContext.callSettings);
131
+ const callSettingsValue = readProperty(modelContext, "callSettings");
132
+ if (callSettingsValue !== void 0) {
133
+ const callSettings = sanitizeAndRedact(callSettingsValue);
109
134
  if (callSettings && typeof callSettings === "object" && !Array.isArray(callSettings)) result.callSettings = callSettings;
110
135
  }
111
- if (modelContext.config !== void 0) {
112
- const config = sanitizeAndRedact(modelContext.config);
136
+ const configValue = readProperty(modelContext, "config");
137
+ if (configValue !== void 0) {
138
+ const config = sanitizeAndRedact(configValue);
113
139
  if (config && typeof config === "object" && !Array.isArray(config)) result.config = config;
114
140
  }
115
141
  return Object.keys(result).length > 0 ? result : void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"serialization.js","names":["ModelContext","SerializedModelContext","normalizeToolList","NormalizedTool","sanitizeForMessage","value","seen","WeakSet","undefined","String","Date","Number","isNaN","getTime","toISOString","Map","has","add","result","Record","key","entry","entries","delete","Set","Array","from","map","isArray","filter","item","Object","REDACTED","SENSITIVE_KEYS","normalizeKey","toLowerCase","replace","MASK_ALL_KEYS","redactSensitive","maskAll","normalized","sanitizeAndRedact","serializeModelContext","context","modelContext","systemValue","system","length","tools","tool","parameters","providerOptions","providerArgs","server","backendDefault","callSettings","config","keys"],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList, type NormalizedTool } from \"./toolNormalization\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return String(value);\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();\n }\n if (value instanceof Map) {\n if (seen.has(value)) return \"[Circular]\";\n seen.add(value);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of value.entries()) {\n result[String(key)] = sanitizeForMessage(entry, seen);\n }\n seen.delete(value);\n return result;\n }\n if (value instanceof Set) {\n if (seen.has(value)) return \"[Circular]\";\n seen.add(value);\n const result = Array.from(value).map((entry) =>\n sanitizeForMessage(entry, seen),\n );\n seen.delete(value);\n return result;\n }\n if (Array.isArray(value)) {\n if (seen.has(value as unknown as object)) return \"[Circular]\";\n seen.add(value as unknown as object);\n const result = value\n .map((entry) => sanitizeForMessage(entry, seen))\n .filter((item) => item !== undefined);\n seen.delete(value as unknown as object);\n return result;\n }\n if (typeof value === \"object\") {\n if (seen.has(value as object)) return \"[Circular]\";\n seen.add(value as object);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n result[key] = sanitizeForMessage(entry, seen);\n }\n seen.delete(value as object);\n return result;\n }\n return value;\n};\n\nexport const REDACTED = \"[redacted]\";\n\nconst SENSITIVE_KEYS = new Set([\n \"apikey\",\n \"xapikey\",\n \"accesskey\",\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"clientsecret\",\n \"token\",\n \"accesstoken\",\n \"refreshtoken\",\n \"cookie\",\n \"setcookie\",\n \"credential\",\n \"credentials\",\n \"privatekey\",\n \"bearer\",\n \"sessionid\",\n]);\n\nconst normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, \"\");\n\n/**\n * Subtrees that are credential maps with arbitrary, user-defined key names\n * (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch\n * `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these\n * is masked wholesale.\n */\nconst MASK_ALL_KEYS = new Set([\"env\", \"headers\"]);\n\n/**\n * Mask values whose key names a known credential, and mask every value inside\n * an `env`/`headers` subtree wholesale. Operates on already sanitized plain\n * data (primitives, arrays, plain objects). Applied only to config-bearing\n * subtrees, never to a tool's parameters schema, so a schema property literally\n * named `token` is not corrupted.\n */\nexport const redactSensitive = (value: unknown, maskAll = false): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => redactSensitive(entry, maskAll));\n }\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n const normalized = normalizeKey(key);\n result[key] =\n maskAll || SENSITIVE_KEYS.has(normalized)\n ? REDACTED\n : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));\n }\n return result;\n }\n return maskAll ? REDACTED : value;\n};\n\nexport const sanitizeAndRedact = (value: unknown): unknown =>\n redactSensitive(sanitizeForMessage(value));\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = modelContext.system;\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(modelContext.tools);\n if (tools.length > 0) {\n result.tools = tools.map((tool): NormalizedTool => {\n return {\n ...tool,\n parameters: sanitizeForMessage(tool.parameters),\n ...(tool.providerOptions !== undefined\n ? { providerOptions: sanitizeAndRedact(tool.providerOptions) }\n : {}),\n ...(tool.providerArgs !== undefined\n ? { providerArgs: sanitizeAndRedact(tool.providerArgs) }\n : {}),\n ...(tool.server !== undefined\n ? { server: sanitizeAndRedact(tool.server) }\n : {}),\n ...(tool.backendDefault !== undefined\n ? { backendDefault: sanitizeForMessage(tool.backendDefault) }\n : {}),\n };\n });\n }\n\n if (modelContext.callSettings !== undefined) {\n const callSettings = sanitizeAndRedact(modelContext.callSettings);\n if (\n callSettings &&\n typeof callSettings === \"object\" &&\n !Array.isArray(callSettings)\n ) {\n result.callSettings = callSettings as Record<string, unknown>;\n }\n }\n\n if (modelContext.config !== undefined) {\n const config = sanitizeAndRedact(modelContext.config);\n if (config && typeof config === \"object\" && !Array.isArray(config)) {\n result.config = config as Record<string, unknown>;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"serialization.js","names":["ModelContext","SerializedModelContext","normalizeToolList","NormalizedTool","readProperty","UNSERIALIZABLE","sanitizeForMessage","value","seen","WeakSet","undefined","String","has","add","Date","Number","isNaN","getTime","toISOString","Map","result","Record","nextSuffixByKey","key","entry","entries","serializedKey","Object","hasOwn","baseKey","suffix","get","set","Set","Array","from","map","isArray","length","index","item","push","keys","delete","REDACTED","SENSITIVE_KEYS","normalizeKey","toLowerCase","replace","MASK_ALL_KEYS","redactSensitive","maskAll","normalized","sanitizeAndRedact","serializeModelContext","context","modelContext","systemValue","system","tools","tool","parameters","providerOptions","providerArgs","server","backendDefault","callSettingsValue","callSettings","configValue","config"],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList, type NormalizedTool } from \"./toolNormalization\";\nimport { readProperty, UNSERIALIZABLE } from \"./unserializable\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return String(value);\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (typeof value === \"object\") {\n if (seen.has(value)) return \"[Circular]\";\n seen.add(value);\n try {\n if (value instanceof Date) {\n return Number.isNaN(value.getTime())\n ? String(value)\n : value.toISOString();\n }\n if (value instanceof Map) {\n const result: Record<string, unknown> = {};\n const nextSuffixByKey = new Map<string, number>();\n for (const [key, entry] of value.entries()) {\n let serializedKey: string;\n try {\n serializedKey = String(key);\n } catch {\n serializedKey = UNSERIALIZABLE;\n }\n\n if (Object.hasOwn(result, serializedKey)) {\n const baseKey = serializedKey;\n let suffix = nextSuffixByKey.get(baseKey) ?? 2;\n do {\n serializedKey = `${baseKey} (${suffix})`;\n suffix += 1;\n } while (Object.hasOwn(result, serializedKey));\n nextSuffixByKey.set(baseKey, suffix);\n } else {\n nextSuffixByKey.set(serializedKey, 2);\n }\n\n result[serializedKey] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n if (value instanceof Set) {\n return Array.from(value).map((entry) =>\n sanitizeForMessage(entry, seen),\n );\n }\n if (Array.isArray(value)) {\n const result: unknown[] = [];\n const length = value.length;\n for (let index = 0; index < length; index++) {\n try {\n if (!(index in value)) continue;\n const item = sanitizeForMessage(value[index], seen);\n if (item !== undefined) result.push(item);\n } catch {\n result.push(UNSERIALIZABLE);\n }\n }\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n try {\n result[key] = sanitizeForMessage(\n (value as Record<string, unknown>)[key],\n seen,\n );\n } catch {\n result[key] = UNSERIALIZABLE;\n }\n }\n return result;\n } catch {\n return UNSERIALIZABLE;\n } finally {\n seen.delete(value);\n }\n }\n return value;\n};\n\nexport const REDACTED = \"[redacted]\";\n\nconst SENSITIVE_KEYS = new Set([\n \"apikey\",\n \"xapikey\",\n \"accesskey\",\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"clientsecret\",\n \"token\",\n \"accesstoken\",\n \"refreshtoken\",\n \"cookie\",\n \"setcookie\",\n \"credential\",\n \"credentials\",\n \"privatekey\",\n \"bearer\",\n \"sessionid\",\n]);\n\nconst normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, \"\");\n\n/**\n * Subtrees that are credential maps with arbitrary, user-defined key names\n * (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch\n * `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these\n * is masked wholesale.\n */\nconst MASK_ALL_KEYS = new Set([\"env\", \"headers\"]);\n\n/**\n * Mask values whose key names a known credential, and mask every value inside\n * an `env`/`headers` subtree wholesale. Operates on already sanitized plain\n * data (primitives, arrays, plain objects). Applied only to config-bearing\n * subtrees, never to a tool's parameters schema, so a schema property literally\n * named `token` is not corrupted.\n */\nexport const redactSensitive = (value: unknown, maskAll = false): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => redactSensitive(entry, maskAll));\n }\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n const normalized = normalizeKey(key);\n result[key] =\n maskAll || SENSITIVE_KEYS.has(normalized)\n ? REDACTED\n : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));\n }\n return result;\n }\n return maskAll ? REDACTED : value;\n};\n\nexport const sanitizeAndRedact = (value: unknown): unknown =>\n redactSensitive(sanitizeForMessage(value));\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = readProperty(modelContext, \"system\");\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(readProperty(modelContext, \"tools\"));\n if (tools.length > 0) {\n result.tools = tools.map((tool): NormalizedTool => {\n return {\n ...tool,\n parameters: sanitizeForMessage(tool.parameters),\n ...(tool.providerOptions !== undefined\n ? { providerOptions: sanitizeAndRedact(tool.providerOptions) }\n : {}),\n ...(tool.providerArgs !== undefined\n ? { providerArgs: sanitizeAndRedact(tool.providerArgs) }\n : {}),\n ...(tool.server !== undefined\n ? { server: sanitizeAndRedact(tool.server) }\n : {}),\n ...(tool.backendDefault !== undefined\n ? { backendDefault: sanitizeForMessage(tool.backendDefault) }\n : {}),\n };\n });\n }\n\n const callSettingsValue = readProperty(modelContext, \"callSettings\");\n if (callSettingsValue !== undefined) {\n const callSettings = sanitizeAndRedact(callSettingsValue);\n if (\n callSettings &&\n typeof callSettings === \"object\" &&\n !Array.isArray(callSettings)\n ) {\n result.callSettings = callSettings as Record<string, unknown>;\n }\n }\n\n const configValue = readProperty(modelContext, \"config\");\n if (configValue !== undefined) {\n const config = sanitizeAndRedact(configValue);\n if (config && typeof config === \"object\" && !Array.isArray(config)) {\n result.config = config as Record<string, unknown>;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"toolNormalization.d.ts","names":[],"sources":["../../src/utils/toolNormalization.ts"],"mappings":";KAEY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;cAuEW,oBAAqB,mBAAiB"}
1
+ {"version":3,"file":"toolNormalization.d.ts","names":[],"sources":["../../src/utils/toolNormalization.ts"],"mappings":";KAGY;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;cAqFW,oBAAqB,mBAAiB"}
@@ -1,9 +1,10 @@
1
+ import { UNSERIALIZABLE, readProperty } from "./unserializable.js";
1
2
  import { z } from "zod";
2
3
  //#region src/utils/toolNormalization.ts
3
4
  const isRecord = (value) => value !== null && typeof value === "object";
4
5
  const toJsonSchema = (value) => {
5
- if (value instanceof z.ZodType) try {
6
- return z.toJSONSchema(value);
6
+ try {
7
+ if (value instanceof z.ZodType) return z.toJSONSchema(value);
7
8
  } catch {
8
9
  return value;
9
10
  }
@@ -11,32 +12,63 @@ const toJsonSchema = (value) => {
11
12
  };
12
13
  const mapToNormalizedTool = (name, raw) => {
13
14
  const tool = { name };
14
- if (typeof raw.type === "string") tool.type = raw.type;
15
- if (typeof raw.description === "string") tool.description = raw.description;
16
- if (typeof raw.disabled === "boolean") tool.disabled = raw.disabled;
17
- if (typeof raw.display === "string") tool.display = raw.display;
18
- if (typeof raw.providerId === "string") tool.providerId = raw.providerId;
19
- if (typeof raw.supportsDeferredResults === "boolean") tool.supportsDeferredResults = raw.supportsDeferredResults;
20
- if (raw.unstable_backendDefault !== void 0) tool.backendDefault = raw.unstable_backendDefault;
21
- if (raw.providerOptions !== void 0) tool.providerOptions = raw.providerOptions;
22
- if (raw.args !== void 0) tool.providerArgs = raw.args;
23
- if (raw.server !== void 0) tool.server = raw.server;
24
- if (Object.hasOwn(raw, "parameters")) tool.parameters = toJsonSchema(raw.parameters);
15
+ const type = readProperty(raw, "type");
16
+ if (typeof type === "string") tool.type = type;
17
+ const description = readProperty(raw, "description");
18
+ if (typeof description === "string") tool.description = description;
19
+ const disabled = readProperty(raw, "disabled");
20
+ if (typeof disabled === "boolean") tool.disabled = disabled;
21
+ const display = readProperty(raw, "display");
22
+ if (typeof display === "string") tool.display = display;
23
+ const providerId = readProperty(raw, "providerId");
24
+ if (typeof providerId === "string") tool.providerId = providerId;
25
+ const supportsDeferredResults = readProperty(raw, "supportsDeferredResults");
26
+ if (typeof supportsDeferredResults === "boolean") tool.supportsDeferredResults = supportsDeferredResults;
27
+ const backendDefault = readProperty(raw, "unstable_backendDefault");
28
+ if (backendDefault !== void 0) tool.backendDefault = backendDefault;
29
+ const providerOptions = readProperty(raw, "providerOptions");
30
+ if (providerOptions !== void 0) tool.providerOptions = providerOptions;
31
+ const providerArgs = readProperty(raw, "args");
32
+ if (providerArgs !== void 0) tool.providerArgs = providerArgs;
33
+ const server = readProperty(raw, "server");
34
+ if (server !== void 0) tool.server = server;
35
+ try {
36
+ if (Object.hasOwn(raw, "parameters")) tool.parameters = toJsonSchema(readProperty(raw, "parameters"));
37
+ } catch {
38
+ tool.parameters = UNSERIALIZABLE;
39
+ }
25
40
  return tool;
26
41
  };
27
42
  const normalizeToolList = (value) => {
43
+ if (value === "[Unserializable]") return [{ name: UNSERIALIZABLE }];
28
44
  if (!value || typeof value !== "object") return [];
29
45
  if (Array.isArray(value)) {
30
46
  const tools = [];
31
- for (const entry of value) {
32
- if (!isRecord(entry) || typeof entry.name !== "string") continue;
33
- tools.push(mapToNormalizedTool(entry.name, entry));
47
+ const length = readProperty(value, "length");
48
+ if (typeof length !== "number") return [{ name: UNSERIALIZABLE }];
49
+ for (let index = 0; index < length; index++) {
50
+ const entry = readProperty(value, index);
51
+ if (entry === "[Unserializable]") {
52
+ tools.push({ name: UNSERIALIZABLE });
53
+ continue;
54
+ }
55
+ if (!isRecord(entry)) continue;
56
+ const name = readProperty(entry, "name");
57
+ if (typeof name !== "string") continue;
58
+ tools.push(mapToNormalizedTool(name, entry));
34
59
  }
35
60
  return tools;
36
61
  }
37
62
  if (isRecord(value)) {
38
63
  const tools = [];
39
- for (const [name, entry] of Object.entries(value)) {
64
+ let names;
65
+ try {
66
+ names = Object.keys(value);
67
+ } catch {
68
+ return [{ name: UNSERIALIZABLE }];
69
+ }
70
+ for (const name of names) {
71
+ const entry = readProperty(value, name);
40
72
  if (!isRecord(entry)) {
41
73
  tools.push({ name });
42
74
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"toolNormalization.js","names":["z","NormalizedTool","name","type","description","disabled","display","providerId","supportsDeferredResults","backendDefault","providerOptions","providerArgs","server","parameters","isRecord","value","Record","toJsonSchema","ZodType","toJSONSchema","mapToNormalizedTool","raw","tool","unstable_backendDefault","undefined","args","Object","hasOwn","normalizeToolList","Array","isArray","tools","entry","push","entries"],"sources":["../../src/utils/toolNormalization.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport type NormalizedTool = {\n name: string;\n type?: string;\n description?: string;\n disabled?: boolean;\n display?: string;\n providerId?: string;\n supportsDeferredResults?: boolean;\n backendDefault?: unknown;\n providerOptions?: unknown;\n providerArgs?: unknown;\n server?: unknown;\n parameters?: unknown;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n\nconst toJsonSchema = (value: unknown): unknown => {\n if (value instanceof z.ZodType) {\n try {\n return z.toJSONSchema(value);\n } catch {\n return value;\n }\n }\n\n return value;\n};\n\nconst mapToNormalizedTool = (\n name: string,\n raw: Record<string, unknown>,\n): NormalizedTool => {\n const tool: NormalizedTool = { name };\n\n if (typeof raw.type === \"string\") {\n tool.type = raw.type as string;\n }\n\n if (typeof raw.description === \"string\") {\n tool.description = raw.description as string;\n }\n\n if (typeof raw.disabled === \"boolean\") {\n tool.disabled = raw.disabled as boolean;\n }\n\n if (typeof raw.display === \"string\") {\n tool.display = raw.display as string;\n }\n\n if (typeof raw.providerId === \"string\") {\n tool.providerId = raw.providerId as string;\n }\n\n if (typeof raw.supportsDeferredResults === \"boolean\") {\n tool.supportsDeferredResults = raw.supportsDeferredResults as boolean;\n }\n\n if (raw.unstable_backendDefault !== undefined) {\n tool.backendDefault = raw.unstable_backendDefault;\n }\n\n if (raw.providerOptions !== undefined) {\n tool.providerOptions = raw.providerOptions;\n }\n\n if (raw.args !== undefined) {\n tool.providerArgs = raw.args;\n }\n\n if (raw.server !== undefined) {\n tool.server = raw.server;\n }\n\n if (Object.hasOwn(raw, \"parameters\")) {\n tool.parameters = toJsonSchema(raw.parameters);\n }\n\n return tool;\n};\n\nexport const normalizeToolList = (value: unknown): NormalizedTool[] => {\n if (!value || typeof value !== \"object\") {\n return [];\n }\n\n if (Array.isArray(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const entry of value) {\n if (!isRecord(entry) || typeof entry.name !== \"string\") continue;\n tools.push(mapToNormalizedTool(entry.name as string, entry));\n }\n\n return tools;\n }\n\n if (isRecord(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const [name, entry] of Object.entries(value)) {\n if (!isRecord(entry)) {\n tools.push({ name });\n continue;\n }\n\n tools.push(mapToNormalizedTool(name, entry));\n }\n\n return tools;\n }\n\n return [];\n};\n"],"mappings":";;AAiBA,MAAMc,YAAYC,UAChBA,UAAU,QAAQ,OAAOA,UAAU;AAErC,MAAME,gBAAgBF,UAA4B;CAChD,IAAIA,iBAAiBf,EAAEkB,SACrB,IAAI;EACF,OAAOlB,EAAEmB,aAAaJ,KAAK;CAC7B,QAAQ;EACN,OAAOA;CACT;CAGF,OAAOA;AACT;AAEA,MAAMK,uBACJlB,MACAmB,QACmB;CACnB,MAAMC,OAAuB,EAAEpB,KAAK;CAEpC,IAAI,OAAOmB,IAAIlB,SAAS,UACtBmB,KAAKnB,OAAOkB,IAAIlB;CAGlB,IAAI,OAAOkB,IAAIjB,gBAAgB,UAC7BkB,KAAKlB,cAAciB,IAAIjB;CAGzB,IAAI,OAAOiB,IAAIhB,aAAa,WAC1BiB,KAAKjB,WAAWgB,IAAIhB;CAGtB,IAAI,OAAOgB,IAAIf,YAAY,UACzBgB,KAAKhB,UAAUe,IAAIf;CAGrB,IAAI,OAAOe,IAAId,eAAe,UAC5Be,KAAKf,aAAac,IAAId;CAGxB,IAAI,OAAOc,IAAIb,4BAA4B,WACzCc,KAAKd,0BAA0Ba,IAAIb;CAGrC,IAAIa,IAAIE,4BAA4BC,KAAAA,GAClCF,KAAKb,iBAAiBY,IAAIE;CAG5B,IAAIF,IAAIX,oBAAoBc,KAAAA,GAC1BF,KAAKZ,kBAAkBW,IAAIX;CAG7B,IAAIW,IAAII,SAASD,KAAAA,GACfF,KAAKX,eAAeU,IAAII;CAG1B,IAAIJ,IAAIT,WAAWY,KAAAA,GACjBF,KAAKV,SAASS,IAAIT;CAGpB,IAAIc,OAAOC,OAAON,KAAK,YAAY,GACjCC,KAAKT,aAAaI,aAAaI,IAAIR,UAAU;CAG/C,OAAOS;AACT;AAEA,MAAaM,qBAAqBb,UAAqC;CACrE,IAAI,CAACA,SAAS,OAAOA,UAAU,UAC7B,OAAO,CAAA;CAGT,IAAIc,MAAMC,QAAQf,KAAK,GAAG;EACxB,MAAMgB,QAA0B,CAAA;EAEhC,KAAK,MAAMC,SAASjB,OAAO;GACzB,IAAI,CAACD,SAASkB,KAAK,KAAK,OAAOA,MAAM9B,SAAS,UAAU;GACxD6B,MAAME,KAAKb,oBAAoBY,MAAM9B,MAAgB8B,KAAK,CAAC;EAC7D;EAEA,OAAOD;CACT;CAEA,IAAIjB,SAASC,KAAK,GAAG;EACnB,MAAMgB,QAA0B,CAAA;EAEhC,KAAK,MAAM,CAAC7B,MAAM8B,UAAUN,OAAOQ,QAAQnB,KAAK,GAAG;GACjD,IAAI,CAACD,SAASkB,KAAK,GAAG;IACpBD,MAAME,KAAK,EAAE/B,KAAK,CAAC;IACnB;GACF;GAEA6B,MAAME,KAAKb,oBAAoBlB,MAAM8B,KAAK,CAAC;EAC7C;EAEA,OAAOD;CACT;CAEA,OAAO,CAAA;AACT"}
1
+ {"version":3,"file":"toolNormalization.js","names":["z","readProperty","UNSERIALIZABLE","NormalizedTool","name","type","description","disabled","display","providerId","supportsDeferredResults","backendDefault","providerOptions","providerArgs","server","parameters","isRecord","value","Record","toJsonSchema","ZodType","toJSONSchema","mapToNormalizedTool","raw","tool","undefined","Object","hasOwn","normalizeToolList","Array","isArray","tools","length","index","entry","push","names","keys"],"sources":["../../src/utils/toolNormalization.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { readProperty, UNSERIALIZABLE } from \"./unserializable\";\n\nexport type NormalizedTool = {\n name: string;\n type?: string;\n description?: string;\n disabled?: boolean;\n display?: string;\n providerId?: string;\n supportsDeferredResults?: boolean;\n backendDefault?: unknown;\n providerOptions?: unknown;\n providerArgs?: unknown;\n server?: unknown;\n parameters?: unknown;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n\nconst toJsonSchema = (value: unknown): unknown => {\n try {\n if (value instanceof z.ZodType) {\n return z.toJSONSchema(value);\n }\n } catch {\n return value;\n }\n\n return value;\n};\n\nconst mapToNormalizedTool = (\n name: string,\n raw: Record<string, unknown>,\n): NormalizedTool => {\n const tool: NormalizedTool = { name };\n\n const type = readProperty(raw, \"type\");\n if (typeof type === \"string\") {\n tool.type = type;\n }\n\n const description = readProperty(raw, \"description\");\n if (typeof description === \"string\") {\n tool.description = description;\n }\n\n const disabled = readProperty(raw, \"disabled\");\n if (typeof disabled === \"boolean\") {\n tool.disabled = disabled;\n }\n\n const display = readProperty(raw, \"display\");\n if (typeof display === \"string\") {\n tool.display = display;\n }\n\n const providerId = readProperty(raw, \"providerId\");\n if (typeof providerId === \"string\") {\n tool.providerId = providerId;\n }\n\n const supportsDeferredResults = readProperty(raw, \"supportsDeferredResults\");\n if (typeof supportsDeferredResults === \"boolean\") {\n tool.supportsDeferredResults = supportsDeferredResults;\n }\n\n const backendDefault = readProperty(raw, \"unstable_backendDefault\");\n if (backendDefault !== undefined) {\n tool.backendDefault = backendDefault;\n }\n\n const providerOptions = readProperty(raw, \"providerOptions\");\n if (providerOptions !== undefined) {\n tool.providerOptions = providerOptions;\n }\n\n const providerArgs = readProperty(raw, \"args\");\n if (providerArgs !== undefined) {\n tool.providerArgs = providerArgs;\n }\n\n const server = readProperty(raw, \"server\");\n if (server !== undefined) {\n tool.server = server;\n }\n\n try {\n if (Object.hasOwn(raw, \"parameters\")) {\n tool.parameters = toJsonSchema(readProperty(raw, \"parameters\"));\n }\n } catch {\n tool.parameters = UNSERIALIZABLE;\n }\n\n return tool;\n};\n\nexport const normalizeToolList = (value: unknown): NormalizedTool[] => {\n if (value === UNSERIALIZABLE) {\n return [{ name: UNSERIALIZABLE }];\n }\n if (!value || typeof value !== \"object\") {\n return [];\n }\n\n if (Array.isArray(value)) {\n const tools: NormalizedTool[] = [];\n const length = readProperty(value, \"length\");\n if (typeof length !== \"number\") return [{ name: UNSERIALIZABLE }];\n\n for (let index = 0; index < length; index++) {\n const entry = readProperty(value, index);\n if (entry === UNSERIALIZABLE) {\n tools.push({ name: UNSERIALIZABLE });\n continue;\n }\n if (!isRecord(entry)) continue;\n const name = readProperty(entry, \"name\");\n if (typeof name !== \"string\") continue;\n tools.push(mapToNormalizedTool(name, entry));\n }\n\n return tools;\n }\n\n if (isRecord(value)) {\n const tools: NormalizedTool[] = [];\n let names: string[];\n try {\n names = Object.keys(value);\n } catch {\n return [{ name: UNSERIALIZABLE }];\n }\n\n for (const name of names) {\n const entry = readProperty(value, name);\n if (!isRecord(entry)) {\n tools.push({ name });\n continue;\n }\n\n tools.push(mapToNormalizedTool(name, entry));\n }\n\n return tools;\n }\n\n return [];\n};\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ //#region src/utils/unserializable.d.ts
2
+ declare const UNSERIALIZABLE = "[Unserializable]";
3
+ declare const readProperty: (value: object, key: PropertyKey) => unknown;
4
+ //#endregion
5
+ export { UNSERIALIZABLE, readProperty };
6
+ //# sourceMappingURL=unserializable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unserializable.d.ts","names":[],"sources":["../../src/utils/unserializable.ts"],"mappings":";cAAa;cAEA,eAAgB,eAAe,KAAK"}
@@ -0,0 +1,13 @@
1
+ //#region src/utils/unserializable.ts
2
+ const UNSERIALIZABLE = "[Unserializable]";
3
+ const readProperty = (value, key) => {
4
+ try {
5
+ return Reflect.get(value, key);
6
+ } catch {
7
+ return UNSERIALIZABLE;
8
+ }
9
+ };
10
+ //#endregion
11
+ export { UNSERIALIZABLE, readProperty };
12
+
13
+ //# sourceMappingURL=unserializable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unserializable.js","names":["UNSERIALIZABLE","readProperty","value","key","PropertyKey","Reflect","get"],"sources":["../../src/utils/unserializable.ts"],"sourcesContent":["export const UNSERIALIZABLE = \"[Unserializable]\";\n\nexport const readProperty = (value: object, key: PropertyKey): unknown => {\n try {\n return Reflect.get(value, key);\n } catch {\n return UNSERIALIZABLE;\n }\n};\n"],"mappings":";AAAA,MAAaA,iBAAiB;AAE9B,MAAaC,gBAAgBC,OAAeC,QAA8B;CACxE,IAAI;EACF,OAAOE,QAAQC,IAAIJ,OAAOC,GAAG;CAC/B,QAAQ;EACN,OAAOH;CACT;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"contextNodes.d.ts","names":[],"sources":["../../../src/views/context/contextNodes.ts"],"mappings":";;;;KAKY;EACN;EAAkB;EAAgB;;EAClC;EAA0B;EAAc,MAAM;;EAC9C;EAAwB;;EACxB;EAAkB;;EAClB;EAAe;;EACf;EAAmB;;KAEb,kBAAkB,SAAS;cAK1B,kBAAmB,MAAM,YAAU;cA4CnC,cAAe"}
1
+ {"version":3,"file":"contextNodes.d.ts","names":[],"sources":["../../../src/views/context/contextNodes.ts"],"mappings":";;;;KAKY;EACN;EAAkB;EAAgB;;EAClC;EAA0B;EAAc,MAAM;;EAC9C;EAAwB;;EACxB;EAAkB;;EAClB;EAAe;;EACf;EAAmB;;KAEb,kBAAkB,SAAS;cAK1B,kBAAmB,MAAM,YAAU;cAwDnC,cAAe"}
@@ -9,11 +9,24 @@ const buildContextNav = (data) => {
9
9
  kind: "system",
10
10
  preview: model.system
11
11
  });
12
- for (const tool of model?.tools ?? []) modelNodes.push({
13
- id: `ctx:tool:${tool.name}`,
14
- kind: "tool",
15
- tool
16
- });
12
+ const nextToolOccurrence = /* @__PURE__ */ new Map();
13
+ const toolNodeIds = /* @__PURE__ */ new Set();
14
+ for (const tool of model?.tools ?? []) {
15
+ const baseId = `ctx:tool:${tool.name}`;
16
+ let occurrence = nextToolOccurrence.get(tool.name) ?? 1;
17
+ let id = occurrence === 1 ? baseId : `${baseId}:${occurrence}`;
18
+ while (toolNodeIds.has(id)) {
19
+ occurrence += 1;
20
+ id = `${baseId}:${occurrence}`;
21
+ }
22
+ nextToolOccurrence.set(tool.name, occurrence + 1);
23
+ toolNodeIds.add(id);
24
+ modelNodes.push({
25
+ id,
26
+ kind: "tool",
27
+ tool
28
+ });
29
+ }
17
30
  if (hasKeys(model?.callSettings)) modelNodes.push({
18
31
  id: "ctx:callSettings",
19
32
  kind: "callSettings"
@@ -1 +1 @@
1
- {"version":3,"file":"contextNodes.js","names":["ApiInfo","NormalizedTool","isRecord","NavGroup","ContextNode","id","kind","preview","tool","ContextNavGroup","hasKeys","value","Record","Boolean","Object","keys","length","buildContextNav","data","model","modelContext","modelNodes","system","push","tools","name","callSettings","config","runtimeNodes","state","mcp","undefined","label","nodes","toolUiCount"],"sources":["../../../src/views/context/contextNodes.ts"],"sourcesContent":["import type { ApiInfo } from \"../../data/types\";\nimport type { NormalizedTool } from \"../../utils/toolNormalization\";\nimport { isRecord } from \"../../utils/common\";\nimport type { NavGroup } from \"../nav\";\n\nexport type ContextNode =\n | { id: \"ctx:system\"; kind: \"system\"; preview: string }\n | { id: `ctx:tool:${string}`; kind: \"tool\"; tool: NormalizedTool }\n | { id: \"ctx:callSettings\"; kind: \"callSettings\" }\n | { id: \"ctx:config\"; kind: \"config\" }\n | { id: \"ctx:mcp\"; kind: \"mcp\" }\n | { id: \"ctx:toolUIs\"; kind: \"toolUIs\" };\n\nexport type ContextNavGroup = NavGroup<ContextNode>;\n\nconst hasKeys = (value: Record<string, unknown> | undefined) =>\n Boolean(value && Object.keys(value).length > 0);\n\nexport const buildContextNav = (data: ApiInfo): ContextNavGroup[] => {\n const model = data.modelContext;\n const modelNodes: ContextNode[] = [];\n\n if (model?.system) {\n modelNodes.push({\n id: \"ctx:system\",\n kind: \"system\",\n preview: model.system,\n });\n }\n\n for (const tool of model?.tools ?? []) {\n modelNodes.push({\n id: `ctx:tool:${tool.name}`,\n kind: \"tool\",\n tool,\n });\n }\n\n if (hasKeys(model?.callSettings)) {\n modelNodes.push({ id: \"ctx:callSettings\", kind: \"callSettings\" });\n }\n\n if (hasKeys(model?.config)) {\n modelNodes.push({ id: \"ctx:config\", kind: \"config\" });\n }\n\n const runtimeNodes: ContextNode[] = [];\n\n if (data.state.mcp !== undefined) {\n runtimeNodes.push({ id: \"ctx:mcp\", kind: \"mcp\" });\n }\n\n if (data.state.tools !== undefined) {\n runtimeNodes.push({ id: \"ctx:toolUIs\", kind: \"toolUIs\" });\n }\n\n return [\n ...(modelNodes.length ? [{ label: \"Model\", nodes: modelNodes }] : []),\n ...(runtimeNodes.length ? [{ label: \"Runtime\", nodes: runtimeNodes }] : []),\n ];\n};\n\nexport const toolUiCount = (value: unknown) => {\n if (!isRecord(value)) return 0;\n return Object.keys(value).length;\n};\n"],"mappings":";;AAeA,MAAMU,WAAWC,UACfE,QAAQF,SAASG,OAAOC,KAAKJ,KAAK,CAAC,CAACK,SAAS,CAAC;AAEhD,MAAaC,mBAAmBC,SAAqC;CACnE,MAAMC,QAAQD,KAAKE;CACnB,MAAMC,aAA4B,CAAA;CAElC,IAAIF,OAAOG,QACTD,WAAWE,KAAK;EACdlB,IAAI;EACJC,MAAM;EACNC,SAASY,MAAMG;CACjB,CAAC;CAGH,KAAK,MAAMd,QAAQW,OAAOK,SAAS,CAAA,GACjCH,WAAWE,KAAK;EACdlB,IAAI,YAAYG,KAAKiB;EACrBnB,MAAM;EACNE;CACF,CAAC;CAGH,IAAIE,QAAQS,OAAOO,YAAY,GAC7BL,WAAWE,KAAK;EAAElB,IAAI;EAAoBC,MAAM;CAAe,CAAC;CAGlE,IAAII,QAAQS,OAAOQ,MAAM,GACvBN,WAAWE,KAAK;EAAElB,IAAI;EAAcC,MAAM;CAAS,CAAC;CAGtD,MAAMsB,eAA8B,CAAA;CAEpC,IAAIV,KAAKW,MAAMC,QAAQC,KAAAA,GACrBH,aAAaL,KAAK;EAAElB,IAAI;EAAWC,MAAM;CAAM,CAAC;CAGlD,IAAIY,KAAKW,MAAML,UAAUO,KAAAA,GACvBH,aAAaL,KAAK;EAAElB,IAAI;EAAeC,MAAM;CAAU,CAAC;CAG1D,OAAO,CACL,GAAIe,WAAWL,SAAS,CAAC;EAAEgB,OAAO;EAASC,OAAOZ;CAAW,CAAC,IAAI,CAAA,GAClE,GAAIO,aAAaZ,SAAS,CAAC;EAAEgB,OAAO;EAAWC,OAAOL;CAAa,CAAC,IAAI,CAAA,CAAG;AAE/E;AAEA,MAAaM,eAAevB,UAAmB;CAC7C,IAAI,CAACT,SAASS,KAAK,GAAG,OAAO;CAC7B,OAAOG,OAAOC,KAAKJ,KAAK,CAAC,CAACK;AAC5B"}
1
+ {"version":3,"file":"contextNodes.js","names":["ApiInfo","NormalizedTool","isRecord","NavGroup","ContextNode","id","kind","preview","tool","ContextNavGroup","hasKeys","value","Record","Boolean","Object","keys","length","buildContextNav","data","model","modelContext","modelNodes","system","push","nextToolOccurrence","Map","toolNodeIds","Set","tools","baseId","name","const","occurrence","get","has","set","add","callSettings","config","runtimeNodes","state","mcp","undefined","label","nodes","toolUiCount"],"sources":["../../../src/views/context/contextNodes.ts"],"sourcesContent":["import type { ApiInfo } from \"../../data/types\";\nimport type { NormalizedTool } from \"../../utils/toolNormalization\";\nimport { isRecord } from \"../../utils/common\";\nimport type { NavGroup } from \"../nav\";\n\nexport type ContextNode =\n | { id: \"ctx:system\"; kind: \"system\"; preview: string }\n | { id: `ctx:tool:${string}`; kind: \"tool\"; tool: NormalizedTool }\n | { id: \"ctx:callSettings\"; kind: \"callSettings\" }\n | { id: \"ctx:config\"; kind: \"config\" }\n | { id: \"ctx:mcp\"; kind: \"mcp\" }\n | { id: \"ctx:toolUIs\"; kind: \"toolUIs\" };\n\nexport type ContextNavGroup = NavGroup<ContextNode>;\n\nconst hasKeys = (value: Record<string, unknown> | undefined) =>\n Boolean(value && Object.keys(value).length > 0);\n\nexport const buildContextNav = (data: ApiInfo): ContextNavGroup[] => {\n const model = data.modelContext;\n const modelNodes: ContextNode[] = [];\n\n if (model?.system) {\n modelNodes.push({\n id: \"ctx:system\",\n kind: \"system\",\n preview: model.system,\n });\n }\n\n const nextToolOccurrence = new Map<string, number>();\n const toolNodeIds = new Set<string>();\n for (const tool of model?.tools ?? []) {\n const baseId = `ctx:tool:${tool.name}` as const;\n let occurrence = nextToolOccurrence.get(tool.name) ?? 1;\n let id: `ctx:tool:${string}` =\n occurrence === 1 ? baseId : `${baseId}:${occurrence}`;\n while (toolNodeIds.has(id)) {\n occurrence += 1;\n id = `${baseId}:${occurrence}`;\n }\n nextToolOccurrence.set(tool.name, occurrence + 1);\n toolNodeIds.add(id);\n modelNodes.push({\n id,\n kind: \"tool\",\n tool,\n });\n }\n\n if (hasKeys(model?.callSettings)) {\n modelNodes.push({ id: \"ctx:callSettings\", kind: \"callSettings\" });\n }\n\n if (hasKeys(model?.config)) {\n modelNodes.push({ id: \"ctx:config\", kind: \"config\" });\n }\n\n const runtimeNodes: ContextNode[] = [];\n\n if (data.state.mcp !== undefined) {\n runtimeNodes.push({ id: \"ctx:mcp\", kind: \"mcp\" });\n }\n\n if (data.state.tools !== undefined) {\n runtimeNodes.push({ id: \"ctx:toolUIs\", kind: \"toolUIs\" });\n }\n\n return [\n ...(modelNodes.length ? [{ label: \"Model\", nodes: modelNodes }] : []),\n ...(runtimeNodes.length ? [{ label: \"Runtime\", nodes: runtimeNodes }] : []),\n ];\n};\n\nexport const toolUiCount = (value: unknown) => {\n if (!isRecord(value)) return 0;\n return Object.keys(value).length;\n};\n"],"mappings":";;AAeA,MAAMU,WAAWC,UACfE,QAAQF,SAASG,OAAOC,KAAKJ,KAAK,CAAC,CAACK,SAAS,CAAC;AAEhD,MAAaC,mBAAmBC,SAAqC;CACnE,MAAMC,QAAQD,KAAKE;CACnB,MAAMC,aAA4B,CAAA;CAElC,IAAIF,OAAOG,QACTD,WAAWE,KAAK;EACdlB,IAAI;EACJC,MAAM;EACNC,SAASY,MAAMG;CACjB,CAAC;CAGH,MAAME,qCAAqB,IAAIC,IAAoB;CACnD,MAAMC,8BAAc,IAAIC,IAAY;CACpC,KAAK,MAAMnB,QAAQW,OAAOS,SAAS,CAAA,GAAI;EACrC,MAAMC,SAAS,YAAYrB,KAAKsB;EAChC,IAAIE,aAAaR,mBAAmBS,IAAIzB,KAAKsB,IAAI,KAAK;EACtD,IAAIzB,KACF2B,eAAe,IAAIH,SAAS,GAAGA,OAAM,GAAIG;EAC3C,OAAON,YAAYQ,IAAI7B,EAAE,GAAG;GAC1B2B,cAAc;GACd3B,KAAK,GAAGwB,OAAM,GAAIG;EACpB;EACAR,mBAAmBW,IAAI3B,KAAKsB,MAAME,aAAa,CAAC;EAChDN,YAAYU,IAAI/B,EAAE;EAClBgB,WAAWE,KAAK;GACdlB;GACAC,MAAM;GACNE;EACF,CAAC;CACH;CAEA,IAAIE,QAAQS,OAAOkB,YAAY,GAC7BhB,WAAWE,KAAK;EAAElB,IAAI;EAAoBC,MAAM;CAAe,CAAC;CAGlE,IAAII,QAAQS,OAAOmB,MAAM,GACvBjB,WAAWE,KAAK;EAAElB,IAAI;EAAcC,MAAM;CAAS,CAAC;CAGtD,MAAMiC,eAA8B,CAAA;CAEpC,IAAIrB,KAAKsB,MAAMC,QAAQC,KAAAA,GACrBH,aAAahB,KAAK;EAAElB,IAAI;EAAWC,MAAM;CAAM,CAAC;CAGlD,IAAIY,KAAKsB,MAAMZ,UAAUc,KAAAA,GACvBH,aAAahB,KAAK;EAAElB,IAAI;EAAeC,MAAM;CAAU,CAAC;CAG1D,OAAO,CACL,GAAIe,WAAWL,SAAS,CAAC;EAAE2B,OAAO;EAASC,OAAOvB;CAAW,CAAC,IAAI,CAAA,GAClE,GAAIkB,aAAavB,SAAS,CAAC;EAAE2B,OAAO;EAAWC,OAAOL;CAAa,CAAC,IAAI,CAAA,CAAG;AAE/E;AAEA,MAAaM,eAAelC,UAAmB;CAC7C,IAAI,CAACT,SAASS,KAAK,GAAG,OAAO;CAC7B,OAAOG,OAAOC,KAAKJ,KAAK,CAAC,CAACK;AAC5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-devtools",
3
- "version": "1.2.16",
3
+ "version": "1.2.17",
4
4
  "description": "React development tools for assistant-ui components",
5
5
  "keywords": [
6
6
  "assistant-ui",
@@ -28,11 +28,11 @@
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
30
  "clsx": "^2.1.1",
31
- "zod": "^4.4.3"
31
+ "zod": "^4.5.4"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@assistant-ui/react": "^0.15.0",
35
- "@assistant-ui/tap": "^0.9.0",
35
+ "@assistant-ui/tap": "^0.9.16",
36
36
  "@types/react": "*",
37
37
  "@types/react-dom": "*",
38
38
  "react": "^18 || ^19",
@@ -47,11 +47,11 @@
47
47
  }
48
48
  },
49
49
  "devDependencies": {
50
- "@assistant-ui/react": "0.15.17",
51
- "@assistant-ui/tap": "0.9.15",
52
- "@assistant-ui/x-buildutils": "0.0.24",
50
+ "@assistant-ui/react": "0.15.18",
51
+ "@assistant-ui/tap": "0.9.16",
52
+ "@assistant-ui/x-buildutils": "0.0.25",
53
53
  "@tailwindcss/cli": "^4.3.3",
54
- "@types/node": "^26.2.0",
54
+ "@types/node": "^26.4.0",
55
55
  "@types/react": "^19.2.18",
56
56
  "@types/react-dom": "^19.2.5",
57
57
  "react": "^19.2.8",
@@ -77,6 +77,45 @@ describe("projectApi", () => {
77
77
  expect(result.modelContext).toEqual({ system: "be nice" });
78
78
  });
79
79
 
80
+ it("keeps readable model-context fields when others are unreadable", () => {
81
+ const tools = new Proxy(
82
+ {},
83
+ {
84
+ ownKeys: () => {
85
+ throw new Error("tools unavailable");
86
+ },
87
+ },
88
+ );
89
+ const modelContext = {
90
+ tools,
91
+ config: { model: "test-model" },
92
+ };
93
+ Object.defineProperty(modelContext, "system", {
94
+ get: () => {
95
+ throw new Error("system unavailable");
96
+ },
97
+ });
98
+ const thread = scope(
99
+ "root",
100
+ {},
101
+ {
102
+ getState: () => ({ messages: [], isRunning: false }),
103
+ getModelContext: () => modelContext,
104
+ },
105
+ );
106
+
107
+ const projected = projectApi(1, {
108
+ api: { thread },
109
+ logs: [],
110
+ } as unknown as Parameters<typeof projectApi>[1]);
111
+
112
+ expect(projected.modelContext).toEqual({
113
+ system: "[Unserializable]",
114
+ tools: [{ name: "[Unserializable]" }],
115
+ config: { model: "test-model" },
116
+ });
117
+ });
118
+
80
119
  it("keeps event-log timestamps as Date instances", () => {
81
120
  expect(result.logs[0]?.time).toBeInstanceOf(Date);
82
121
  expect(result.logs[0]?.event).toBe("thread.run-start");
@@ -59,6 +59,85 @@ describe("sanitizeForMessage", () => {
59
59
  it("sanitizes invalid dates without throwing", () => {
60
60
  expect(sanitizeForMessage(new Date(Number.NaN))).toBe("Invalid Date");
61
61
  });
62
+
63
+ it("preserves readable properties when an enumerable getter throws", () => {
64
+ const value = { readable: "value" };
65
+ Object.defineProperty(value, "broken", {
66
+ enumerable: true,
67
+ get: () => {
68
+ throw new Error("getter failed");
69
+ },
70
+ });
71
+
72
+ expect(sanitizeForMessage(value)).toEqual({
73
+ readable: "value",
74
+ broken: "[Unserializable]",
75
+ });
76
+ });
77
+
78
+ it("handles proxies that reject key enumeration", () => {
79
+ const value = new Proxy(
80
+ {},
81
+ {
82
+ ownKeys: () => {
83
+ throw new Error("enumeration failed");
84
+ },
85
+ },
86
+ );
87
+
88
+ expect(sanitizeForMessage(value)).toBe("[Unserializable]");
89
+ });
90
+
91
+ it("preserves readable array entries when an indexed getter throws", () => {
92
+ const value = ["first", "second", "third"];
93
+ Object.defineProperty(value, 1, {
94
+ get: () => {
95
+ throw new Error("getter failed");
96
+ },
97
+ });
98
+
99
+ expect(sanitizeForMessage(value)).toEqual([
100
+ "first",
101
+ "[Unserializable]",
102
+ "third",
103
+ ]);
104
+ });
105
+
106
+ it("snapshots proxy array length before reading entries", () => {
107
+ let lengthReads = 0;
108
+ const value = new Proxy(["first", "second"], {
109
+ get: (target, property, receiver) => {
110
+ if (property === "length") return lengthReads++ === 0 ? 2 : 0;
111
+ return Reflect.get(target, property, receiver);
112
+ },
113
+ });
114
+
115
+ expect(sanitizeForMessage(value)).toEqual(["first", "second"]);
116
+ });
117
+
118
+ it("preserves map entries when a key cannot be converted to a string", () => {
119
+ const brokenKey = {
120
+ toString: () => {
121
+ throw new Error("key conversion failed");
122
+ },
123
+ };
124
+ const secondBrokenKey = {
125
+ toString: () => {
126
+ throw new Error("key conversion failed");
127
+ },
128
+ };
129
+ const value = new Map<unknown, unknown>([
130
+ [brokenKey, "broken key value"],
131
+ [secondBrokenKey, "second broken key value"],
132
+ ["readable", "readable value"],
133
+ ]);
134
+
135
+ expect(sanitizeForMessage(value)).toEqual({
136
+ "[Unserializable]": "broken key value",
137
+ "[Unserializable] (2)": "second broken key value",
138
+ readable: "readable value",
139
+ });
140
+ });
62
141
  });
63
142
 
64
143
  describe("redactSensitive", () => {
@@ -181,4 +260,31 @@ describe("serializeModelContext", () => {
181
260
  it("returns undefined when there is no context", () => {
182
261
  expect(serializeModelContext(undefined)).toBeUndefined();
183
262
  });
263
+
264
+ it("preserves readable fields when a model-context getter throws", () => {
265
+ const context = {
266
+ tools: {
267
+ search: { type: "frontend", description: "Search documents" },
268
+ },
269
+ config: { model: "test-model" },
270
+ };
271
+ Object.defineProperty(context, "system", {
272
+ enumerable: true,
273
+ get: () => {
274
+ throw new Error("system unavailable");
275
+ },
276
+ });
277
+
278
+ expect(serializeModelContext(context as never)).toEqual({
279
+ system: "[Unserializable]",
280
+ tools: [
281
+ {
282
+ name: "search",
283
+ type: "frontend",
284
+ description: "Search documents",
285
+ },
286
+ ],
287
+ config: { model: "test-model" },
288
+ });
289
+ });
184
290
  });
@@ -1,6 +1,7 @@
1
1
  import type { ModelContext } from "@assistant-ui/react";
2
2
  import type { SerializedModelContext } from "../types";
3
3
  import { normalizeToolList, type NormalizedTool } from "./toolNormalization";
4
+ import { readProperty, UNSERIALIZABLE } from "./unserializable";
4
5
 
5
6
  export const sanitizeForMessage = (
6
7
  value: unknown,
@@ -21,48 +22,79 @@ export const sanitizeForMessage = (
21
22
  if (typeof value === "function") {
22
23
  return "[Function]";
23
24
  }
24
- if (value instanceof Date) {
25
- return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
26
- }
27
- if (value instanceof Map) {
28
- if (seen.has(value)) return "[Circular]";
29
- seen.add(value);
30
- const result: Record<string, unknown> = {};
31
- for (const [key, entry] of value.entries()) {
32
- result[String(key)] = sanitizeForMessage(entry, seen);
33
- }
34
- seen.delete(value);
35
- return result;
36
- }
37
- if (value instanceof Set) {
25
+ if (typeof value === "object") {
38
26
  if (seen.has(value)) return "[Circular]";
39
27
  seen.add(value);
40
- const result = Array.from(value).map((entry) =>
41
- sanitizeForMessage(entry, seen),
42
- );
43
- seen.delete(value);
44
- return result;
45
- }
46
- if (Array.isArray(value)) {
47
- if (seen.has(value as unknown as object)) return "[Circular]";
48
- seen.add(value as unknown as object);
49
- const result = value
50
- .map((entry) => sanitizeForMessage(entry, seen))
51
- .filter((item) => item !== undefined);
52
- seen.delete(value as unknown as object);
53
- return result;
54
- }
55
- if (typeof value === "object") {
56
- if (seen.has(value as object)) return "[Circular]";
57
- seen.add(value as object);
58
- const result: Record<string, unknown> = {};
59
- for (const [key, entry] of Object.entries(
60
- value as Record<string, unknown>,
61
- )) {
62
- result[key] = sanitizeForMessage(entry, seen);
28
+ try {
29
+ if (value instanceof Date) {
30
+ return Number.isNaN(value.getTime())
31
+ ? String(value)
32
+ : value.toISOString();
33
+ }
34
+ if (value instanceof Map) {
35
+ const result: Record<string, unknown> = {};
36
+ const nextSuffixByKey = new Map<string, number>();
37
+ for (const [key, entry] of value.entries()) {
38
+ let serializedKey: string;
39
+ try {
40
+ serializedKey = String(key);
41
+ } catch {
42
+ serializedKey = UNSERIALIZABLE;
43
+ }
44
+
45
+ if (Object.hasOwn(result, serializedKey)) {
46
+ const baseKey = serializedKey;
47
+ let suffix = nextSuffixByKey.get(baseKey) ?? 2;
48
+ do {
49
+ serializedKey = `${baseKey} (${suffix})`;
50
+ suffix += 1;
51
+ } while (Object.hasOwn(result, serializedKey));
52
+ nextSuffixByKey.set(baseKey, suffix);
53
+ } else {
54
+ nextSuffixByKey.set(serializedKey, 2);
55
+ }
56
+
57
+ result[serializedKey] = sanitizeForMessage(entry, seen);
58
+ }
59
+ return result;
60
+ }
61
+ if (value instanceof Set) {
62
+ return Array.from(value).map((entry) =>
63
+ sanitizeForMessage(entry, seen),
64
+ );
65
+ }
66
+ if (Array.isArray(value)) {
67
+ const result: unknown[] = [];
68
+ const length = value.length;
69
+ for (let index = 0; index < length; index++) {
70
+ try {
71
+ if (!(index in value)) continue;
72
+ const item = sanitizeForMessage(value[index], seen);
73
+ if (item !== undefined) result.push(item);
74
+ } catch {
75
+ result.push(UNSERIALIZABLE);
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+
81
+ const result: Record<string, unknown> = {};
82
+ for (const key of Object.keys(value)) {
83
+ try {
84
+ result[key] = sanitizeForMessage(
85
+ (value as Record<string, unknown>)[key],
86
+ seen,
87
+ );
88
+ } catch {
89
+ result[key] = UNSERIALIZABLE;
90
+ }
91
+ }
92
+ return result;
93
+ } catch {
94
+ return UNSERIALIZABLE;
95
+ } finally {
96
+ seen.delete(value);
63
97
  }
64
- seen.delete(value as object);
65
- return result;
66
98
  }
67
99
  return value;
68
100
  };
@@ -140,12 +172,12 @@ export const serializeModelContext = (
140
172
  const modelContext = context as Record<string, unknown>;
141
173
  const result: SerializedModelContext = {};
142
174
 
143
- const systemValue = modelContext.system;
175
+ const systemValue = readProperty(modelContext, "system");
144
176
  if (typeof systemValue === "string" && systemValue.length > 0) {
145
177
  result.system = systemValue;
146
178
  }
147
179
 
148
- const tools = normalizeToolList(modelContext.tools);
180
+ const tools = normalizeToolList(readProperty(modelContext, "tools"));
149
181
  if (tools.length > 0) {
150
182
  result.tools = tools.map((tool): NormalizedTool => {
151
183
  return {
@@ -167,8 +199,9 @@ export const serializeModelContext = (
167
199
  });
168
200
  }
169
201
 
170
- if (modelContext.callSettings !== undefined) {
171
- const callSettings = sanitizeAndRedact(modelContext.callSettings);
202
+ const callSettingsValue = readProperty(modelContext, "callSettings");
203
+ if (callSettingsValue !== undefined) {
204
+ const callSettings = sanitizeAndRedact(callSettingsValue);
172
205
  if (
173
206
  callSettings &&
174
207
  typeof callSettings === "object" &&
@@ -178,8 +211,9 @@ export const serializeModelContext = (
178
211
  }
179
212
  }
180
213
 
181
- if (modelContext.config !== undefined) {
182
- const config = sanitizeAndRedact(modelContext.config);
214
+ const configValue = readProperty(modelContext, "config");
215
+ if (configValue !== undefined) {
216
+ const config = sanitizeAndRedact(configValue);
183
217
  if (config && typeof config === "object" && !Array.isArray(config)) {
184
218
  result.config = config as Record<string, unknown>;
185
219
  }
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { normalizeToolList } from "./toolNormalization";
3
4
 
@@ -64,8 +65,73 @@ describe("normalizeToolList", () => {
64
65
  expect(tools[1]?.disabled).toBe(true);
65
66
  });
66
67
 
68
+ it("preserves array entries around an unreadable slot", () => {
69
+ const tools = [
70
+ { name: "first", type: "frontend" },
71
+ { name: "hidden-one", type: "frontend" },
72
+ { name: "hidden-two", type: "frontend" },
73
+ { name: "last", type: "backend" },
74
+ ];
75
+ Object.defineProperty(tools, 1, {
76
+ get: () => {
77
+ throw new Error("tool unavailable");
78
+ },
79
+ });
80
+ Object.defineProperty(tools, 2, {
81
+ get: () => {
82
+ throw new Error("tool unavailable");
83
+ },
84
+ });
85
+
86
+ expect(normalizeToolList(tools)).toEqual([
87
+ { name: "first", type: "frontend" },
88
+ { name: "[Unserializable]" },
89
+ { name: "[Unserializable]" },
90
+ { name: "last", type: "backend" },
91
+ ]);
92
+ });
93
+
94
+ it("keeps Zod schemas that cannot be converted to JSON Schema", () => {
95
+ const parameters = z.object({ when: z.date() });
96
+
97
+ expect(normalizeToolList({ schedule: { parameters } })[0]?.parameters).toBe(
98
+ parameters,
99
+ );
100
+ });
101
+
67
102
  it("returns an empty list for non-objects", () => {
68
103
  expect(normalizeToolList(undefined)).toEqual([]);
69
104
  expect(normalizeToolList(null)).toEqual([]);
70
105
  });
106
+
107
+ it("preserves readable tool properties when another getter throws", () => {
108
+ const tool = { description: "Search documents" };
109
+ Object.defineProperty(tool, "type", {
110
+ enumerable: true,
111
+ get: () => {
112
+ throw new Error("type unavailable");
113
+ },
114
+ });
115
+
116
+ expect(normalizeToolList({ search: tool })).toEqual([
117
+ {
118
+ name: "search",
119
+ type: "[Unserializable]",
120
+ description: "Search documents",
121
+ },
122
+ ]);
123
+ });
124
+
125
+ it("represents a tool collection that rejects enumeration", () => {
126
+ const tools = new Proxy(
127
+ {},
128
+ {
129
+ ownKeys: () => {
130
+ throw new Error("enumeration unavailable");
131
+ },
132
+ },
133
+ );
134
+
135
+ expect(normalizeToolList(tools)).toEqual([{ name: "[Unserializable]" }]);
136
+ });
71
137
  });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { readProperty, UNSERIALIZABLE } from "./unserializable";
2
3
 
3
4
  export type NormalizedTool = {
4
5
  name: string;
@@ -19,12 +20,12 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
19
20
  value !== null && typeof value === "object";
20
21
 
21
22
  const toJsonSchema = (value: unknown): unknown => {
22
- if (value instanceof z.ZodType) {
23
- try {
23
+ try {
24
+ if (value instanceof z.ZodType) {
24
25
  return z.toJSONSchema(value);
25
- } catch {
26
- return value;
27
26
  }
27
+ } catch {
28
+ return value;
28
29
  }
29
30
 
30
31
  return value;
@@ -36,64 +37,90 @@ const mapToNormalizedTool = (
36
37
  ): NormalizedTool => {
37
38
  const tool: NormalizedTool = { name };
38
39
 
39
- if (typeof raw.type === "string") {
40
- tool.type = raw.type as string;
40
+ const type = readProperty(raw, "type");
41
+ if (typeof type === "string") {
42
+ tool.type = type;
41
43
  }
42
44
 
43
- if (typeof raw.description === "string") {
44
- tool.description = raw.description as string;
45
+ const description = readProperty(raw, "description");
46
+ if (typeof description === "string") {
47
+ tool.description = description;
45
48
  }
46
49
 
47
- if (typeof raw.disabled === "boolean") {
48
- tool.disabled = raw.disabled as boolean;
50
+ const disabled = readProperty(raw, "disabled");
51
+ if (typeof disabled === "boolean") {
52
+ tool.disabled = disabled;
49
53
  }
50
54
 
51
- if (typeof raw.display === "string") {
52
- tool.display = raw.display as string;
55
+ const display = readProperty(raw, "display");
56
+ if (typeof display === "string") {
57
+ tool.display = display;
53
58
  }
54
59
 
55
- if (typeof raw.providerId === "string") {
56
- tool.providerId = raw.providerId as string;
60
+ const providerId = readProperty(raw, "providerId");
61
+ if (typeof providerId === "string") {
62
+ tool.providerId = providerId;
57
63
  }
58
64
 
59
- if (typeof raw.supportsDeferredResults === "boolean") {
60
- tool.supportsDeferredResults = raw.supportsDeferredResults as boolean;
65
+ const supportsDeferredResults = readProperty(raw, "supportsDeferredResults");
66
+ if (typeof supportsDeferredResults === "boolean") {
67
+ tool.supportsDeferredResults = supportsDeferredResults;
61
68
  }
62
69
 
63
- if (raw.unstable_backendDefault !== undefined) {
64
- tool.backendDefault = raw.unstable_backendDefault;
70
+ const backendDefault = readProperty(raw, "unstable_backendDefault");
71
+ if (backendDefault !== undefined) {
72
+ tool.backendDefault = backendDefault;
65
73
  }
66
74
 
67
- if (raw.providerOptions !== undefined) {
68
- tool.providerOptions = raw.providerOptions;
75
+ const providerOptions = readProperty(raw, "providerOptions");
76
+ if (providerOptions !== undefined) {
77
+ tool.providerOptions = providerOptions;
69
78
  }
70
79
 
71
- if (raw.args !== undefined) {
72
- tool.providerArgs = raw.args;
80
+ const providerArgs = readProperty(raw, "args");
81
+ if (providerArgs !== undefined) {
82
+ tool.providerArgs = providerArgs;
73
83
  }
74
84
 
75
- if (raw.server !== undefined) {
76
- tool.server = raw.server;
85
+ const server = readProperty(raw, "server");
86
+ if (server !== undefined) {
87
+ tool.server = server;
77
88
  }
78
89
 
79
- if (Object.hasOwn(raw, "parameters")) {
80
- tool.parameters = toJsonSchema(raw.parameters);
90
+ try {
91
+ if (Object.hasOwn(raw, "parameters")) {
92
+ tool.parameters = toJsonSchema(readProperty(raw, "parameters"));
93
+ }
94
+ } catch {
95
+ tool.parameters = UNSERIALIZABLE;
81
96
  }
82
97
 
83
98
  return tool;
84
99
  };
85
100
 
86
101
  export const normalizeToolList = (value: unknown): NormalizedTool[] => {
102
+ if (value === UNSERIALIZABLE) {
103
+ return [{ name: UNSERIALIZABLE }];
104
+ }
87
105
  if (!value || typeof value !== "object") {
88
106
  return [];
89
107
  }
90
108
 
91
109
  if (Array.isArray(value)) {
92
110
  const tools: NormalizedTool[] = [];
111
+ const length = readProperty(value, "length");
112
+ if (typeof length !== "number") return [{ name: UNSERIALIZABLE }];
93
113
 
94
- for (const entry of value) {
95
- if (!isRecord(entry) || typeof entry.name !== "string") continue;
96
- tools.push(mapToNormalizedTool(entry.name as string, entry));
114
+ for (let index = 0; index < length; index++) {
115
+ const entry = readProperty(value, index);
116
+ if (entry === UNSERIALIZABLE) {
117
+ tools.push({ name: UNSERIALIZABLE });
118
+ continue;
119
+ }
120
+ if (!isRecord(entry)) continue;
121
+ const name = readProperty(entry, "name");
122
+ if (typeof name !== "string") continue;
123
+ tools.push(mapToNormalizedTool(name, entry));
97
124
  }
98
125
 
99
126
  return tools;
@@ -101,8 +128,15 @@ export const normalizeToolList = (value: unknown): NormalizedTool[] => {
101
128
 
102
129
  if (isRecord(value)) {
103
130
  const tools: NormalizedTool[] = [];
131
+ let names: string[];
132
+ try {
133
+ names = Object.keys(value);
134
+ } catch {
135
+ return [{ name: UNSERIALIZABLE }];
136
+ }
104
137
 
105
- for (const [name, entry] of Object.entries(value)) {
138
+ for (const name of names) {
139
+ const entry = readProperty(value, name);
106
140
  if (!isRecord(entry)) {
107
141
  tools.push({ name });
108
142
  continue;
@@ -0,0 +1,9 @@
1
+ export const UNSERIALIZABLE = "[Unserializable]";
2
+
3
+ export const readProperty = (value: object, key: PropertyKey): unknown => {
4
+ try {
5
+ return Reflect.get(value, key);
6
+ } catch {
7
+ return UNSERIALIZABLE;
8
+ }
9
+ };
@@ -28,4 +28,25 @@ describe("buildContextNav", () => {
28
28
  "ctx:toolUIs",
29
29
  ]);
30
30
  });
31
+
32
+ it("assigns unique navigation IDs to tools with the same display name", () => {
33
+ const groups = buildContextNav({
34
+ id: 1,
35
+ state: {},
36
+ logs: [],
37
+ modelContext: {
38
+ tools: [
39
+ { name: "[Unserializable]" },
40
+ { name: "[Unserializable]:2" },
41
+ { name: "[Unserializable]" },
42
+ ],
43
+ },
44
+ });
45
+
46
+ expect(flattenNav(groups).map((node) => node.id)).toEqual([
47
+ "ctx:tool:[Unserializable]",
48
+ "ctx:tool:[Unserializable]:2",
49
+ "ctx:tool:[Unserializable]:3",
50
+ ]);
51
+ });
31
52
  });
@@ -28,9 +28,21 @@ export const buildContextNav = (data: ApiInfo): ContextNavGroup[] => {
28
28
  });
29
29
  }
30
30
 
31
+ const nextToolOccurrence = new Map<string, number>();
32
+ const toolNodeIds = new Set<string>();
31
33
  for (const tool of model?.tools ?? []) {
34
+ const baseId = `ctx:tool:${tool.name}` as const;
35
+ let occurrence = nextToolOccurrence.get(tool.name) ?? 1;
36
+ let id: `ctx:tool:${string}` =
37
+ occurrence === 1 ? baseId : `${baseId}:${occurrence}`;
38
+ while (toolNodeIds.has(id)) {
39
+ occurrence += 1;
40
+ id = `${baseId}:${occurrence}`;
41
+ }
42
+ nextToolOccurrence.set(tool.name, occurrence + 1);
43
+ toolNodeIds.add(id);
32
44
  modelNodes.push({
33
- id: `ctx:tool:${tool.name}`,
45
+ id,
34
46
  kind: "tool",
35
47
  tool,
36
48
  });