@assistant-ui/react-devtools 1.2.16 → 1.2.18
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.
- package/dist/shell/DevToolsPanel.js +128 -143
- package/dist/shell/DevToolsPanel.js.map +1 -1
- package/dist/shell/tabs/ThreadTab.js +214 -235
- package/dist/shell/tabs/ThreadTab.js.map +1 -1
- package/dist/utils/serialization.d.ts.map +1 -1
- package/dist/utils/serialization.js +59 -33
- package/dist/utils/serialization.js.map +1 -1
- package/dist/utils/toolNormalization.d.ts.map +1 -1
- package/dist/utils/toolNormalization.js +49 -17
- package/dist/utils/toolNormalization.js.map +1 -1
- package/dist/utils/unserializable.d.ts +6 -0
- package/dist/utils/unserializable.d.ts.map +1 -0
- package/dist/utils/unserializable.js +13 -0
- package/dist/utils/unserializable.js.map +1 -0
- package/dist/views/context/contextNodes.d.ts.map +1 -1
- package/dist/views/context/contextNodes.js +18 -5
- package/dist/views/context/contextNodes.js.map +1 -1
- package/package.json +8 -8
- package/src/data/projectApi.test.ts +39 -0
- package/src/shell/DevToolsPanel.tsx +7 -5
- package/src/shell/tabs/ThreadTab.tsx +10 -6
- package/src/utils/serialization.test.ts +106 -0
- package/src/utils/serialization.ts +79 -45
- package/src/utils/toolNormalization.test.ts +66 -0
- package/src/utils/toolNormalization.ts +64 -30
- package/src/utils/unserializable.ts +9 -0
- package/src/views/context/contextNodes.test.ts +21 -0
- package/src/views/context/contextNodes.ts +13 -1
|
@@ -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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
40
|
-
|
|
40
|
+
const type = readProperty(raw, "type");
|
|
41
|
+
if (typeof type === "string") {
|
|
42
|
+
tool.type = type;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const description = readProperty(raw, "description");
|
|
46
|
+
if (typeof description === "string") {
|
|
47
|
+
tool.description = description;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
const disabled = readProperty(raw, "disabled");
|
|
51
|
+
if (typeof disabled === "boolean") {
|
|
52
|
+
tool.disabled = disabled;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
const display = readProperty(raw, "display");
|
|
56
|
+
if (typeof display === "string") {
|
|
57
|
+
tool.display = display;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
const providerId = readProperty(raw, "providerId");
|
|
61
|
+
if (typeof providerId === "string") {
|
|
62
|
+
tool.providerId = providerId;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const supportsDeferredResults = readProperty(raw, "supportsDeferredResults");
|
|
66
|
+
if (typeof supportsDeferredResults === "boolean") {
|
|
67
|
+
tool.supportsDeferredResults = supportsDeferredResults;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const backendDefault = readProperty(raw, "unstable_backendDefault");
|
|
71
|
+
if (backendDefault !== undefined) {
|
|
72
|
+
tool.backendDefault = backendDefault;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const providerOptions = readProperty(raw, "providerOptions");
|
|
76
|
+
if (providerOptions !== undefined) {
|
|
77
|
+
tool.providerOptions = providerOptions;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
const providerArgs = readProperty(raw, "args");
|
|
81
|
+
if (providerArgs !== undefined) {
|
|
82
|
+
tool.providerArgs = providerArgs;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const server = readProperty(raw, "server");
|
|
86
|
+
if (server !== undefined) {
|
|
87
|
+
tool.server = server;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
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 (
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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;
|
|
@@ -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
|
|
45
|
+
id,
|
|
34
46
|
kind: "tool",
|
|
35
47
|
tool,
|
|
36
48
|
});
|