@fluidframework/tree-agent 2.70.0-360374 → 2.70.0-361092

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/src/agent.ts CHANGED
@@ -17,7 +17,15 @@ import type {
17
17
  } from "@fluidframework/tree/alpha";
18
18
  import { ObjectNodeSchema, Tree } from "@fluidframework/tree/alpha";
19
19
 
20
- import type { SharedTreeChatModel, EditResult, SemanticAgentOptions, Logger } from "./api.js";
20
+ import type {
21
+ SharedTreeChatModel,
22
+ EditResult,
23
+ SemanticAgentOptions,
24
+ Logger,
25
+ SynchronousEditor,
26
+ AsynchronousEditor,
27
+ Context,
28
+ } from "./api.js";
21
29
  import { getPrompt, stringifyTree } from "./prompt.js";
22
30
  import { Subtree } from "./subtree.js";
23
31
  import {
@@ -137,8 +145,7 @@ export class SharedTreeSemanticAgent<TSchema extends ImplicitFieldSchema> {
137
145
  const editResult = await applyTreeFunction(
138
146
  queryTree,
139
147
  editCode,
140
- this.options?.validateEdit ?? defaultValidateEdit,
141
- this.options?.executeEdit ?? defaultExecuteEdit,
148
+ this.options?.editor ?? defaultEditor,
142
149
  this.options?.logger,
143
150
  );
144
151
 
@@ -199,51 +206,23 @@ function constructTreeNode(schema: TreeNodeSchema, value: FactoryContentObject):
199
206
  async function applyTreeFunction<TSchema extends ImplicitFieldSchema>(
200
207
  tree: Subtree<TSchema>,
201
208
  editCode: string,
202
- validateEdit: Required<SemanticAgentOptions>["validateEdit"],
203
- executeEdit: Required<SemanticAgentOptions>["executeEdit"],
209
+ editor: Required<SemanticAgentOptions>["editor"],
204
210
  logger: Logger | undefined,
205
211
  ): Promise<EditResult> {
206
212
  logger?.log(`### Editing Tool Invoked\n\n`);
207
213
  logger?.log(`#### Generated Code\n\n\`\`\`javascript\n${editCode}\n\`\`\`\n\n`);
208
214
 
209
- try {
210
- await validateEdit(editCode);
211
- } catch (error: unknown) {
212
- logger?.log(`#### Code Validation Failed\n\n`);
213
- logger?.log(`\`\`\`JSON\n${toErrorString(error)}\n\`\`\`\n\n`);
214
- return {
215
- type: "validationError",
216
- message: `The generated code did not pass validation: ${toErrorString(error)}`,
217
- };
218
- }
219
-
220
- // Stick the tree schema constructors on an object passed to the function so that the LLM can create new nodes.
221
- const create: Record<string, (input: FactoryContentObject) => TreeNode> = {};
222
- for (const schema of findNamedSchemas(tree.schema)) {
223
- const name = getFriendlyName(schema);
224
- create[name] = (input: FactoryContentObject) => constructTreeNode(schema, input);
225
- }
226
-
227
215
  // Fork a branch to edit. If the edit fails or produces an error, we discard this branch, otherwise we merge it.
228
216
  const editTree = tree.fork();
229
- const context = {
230
- get root(): ReadableField<TSchema> {
231
- return editTree.field;
232
- },
233
- set root(value: TreeFieldFromImplicitField<ReadSchema<TSchema>>) {
234
- editTree.field = value;
235
- },
236
- create,
237
- };
238
-
217
+ const boundEditor = bindEditorToSubtree(editTree, editor);
239
218
  try {
240
- await executeEdit(context, editCode);
219
+ await boundEditor(editCode);
241
220
  } catch (error: unknown) {
242
221
  logger?.log(`#### Error\n\n`);
243
222
  logger?.log(`\`\`\`JSON\n${toErrorString(error)}\n\`\`\`\n\n`);
244
223
  editTree.branch.dispose();
245
224
  return {
246
- type: "executionError",
225
+ type: "editingError",
247
226
  message: `Running the generated code produced an error. The state of the tree will be reset to its previous state as it was before the code ran. Please try again. Here is the error: ${toErrorString(error)}`,
248
227
  };
249
228
  }
@@ -257,13 +236,87 @@ async function applyTreeFunction<TSchema extends ImplicitFieldSchema>(
257
236
  };
258
237
  }
259
238
 
260
- const defaultValidateEdit: Required<SemanticAgentOptions>["validateEdit"] = () => {};
261
-
262
- const defaultExecuteEdit: Required<SemanticAgentOptions>["executeEdit"] = async (
263
- context,
264
- code,
265
- ) => {
239
+ /**
240
+ * The default {@link AsynchronousEditor | editor} implementation that simply uses `new Function` to run the provided code.
241
+ * @remarks This editor allows both synchronous and asynchronous code (i.e. the provided code may return a Promise).
242
+ * @example `await new Function("context", code)(context);`
243
+ * @alpha
244
+ */
245
+ export const defaultEditor: AsynchronousEditor = async (context, code) => {
266
246
  // eslint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval
267
247
  const fn = new Function("context", code);
268
248
  await fn(context);
269
249
  };
250
+
251
+ /**
252
+ * Binds the given {@link AsynchronousEditor | editor} to the given view or tree.
253
+ * @returns A function that takes a string of JavaScript code and executes it on the given view or tree using the given editor function.
254
+ * @remarks This is useful for testing/debugging code execution without needing to set up a full {@link SharedTreeSemanticAgent | agent}.
255
+ * @alpha
256
+ */
257
+ export function bindEditorImpl<TSchema extends ImplicitFieldSchema>(
258
+ tree: TreeView<TSchema> | (ReadableField<TSchema> & TreeNode),
259
+ editor: AsynchronousEditor,
260
+ ): (code: string) => Promise<void>;
261
+ /**
262
+ * Binds the given {@link SynchronousEditor | editor} to the given view or tree.
263
+ * @returns A function that takes a string of JavaScript code and executes it on the given view or tree using the given editor function.
264
+ * @remarks This is useful for testing/debugging code execution without needing to set up a full {@link SharedTreeSemanticAgent | agent}.
265
+ * @alpha
266
+ */
267
+ export function bindEditorImpl<TSchema extends ImplicitFieldSchema>(
268
+ tree: TreeView<TSchema> | (ReadableField<TSchema> & TreeNode),
269
+ editor: SynchronousEditor,
270
+ ): (code: string) => void;
271
+ /**
272
+ * Binds the given {@link SynchronousEditor | editor} or {@link AsynchronousEditor | editor} to the given view or tree.
273
+ * @returns A function that takes a string of JavaScript code and executes it on the given view or tree using the given editor function.
274
+ * @remarks This is useful for testing/debugging code execution without needing to set up a full {@link SharedTreeSemanticAgent | agent}.
275
+ * @alpha
276
+ */
277
+ export function bindEditorImpl<TSchema extends ImplicitFieldSchema>(
278
+ tree: TreeView<TSchema> | (ReadableField<TSchema> & TreeNode),
279
+ editor: SynchronousEditor | AsynchronousEditor,
280
+ ): ((code: string) => void) | ((code: string) => Promise<void>) {
281
+ const subtree = new Subtree(tree);
282
+ return bindEditorToSubtree(subtree, editor);
283
+ }
284
+
285
+ /**
286
+ * Binds the given {@link SynchronousEditor | synchronous} or {@link AsynchronousEditor | asynchronous} editor to the given view or tree.
287
+ * @returns A function that takes a string of JavaScript code and executes it on the given view or tree using the given editor.
288
+ * @remarks This is useful for testing/debugging code execution without needing to set up a full {@link SharedTreeSemanticAgent | agent}.
289
+ * @alpha
290
+ * @privateRemarks This exists (as opposed to just exporting bindEditorImpl directly) so that API documentation links work correctly.
291
+ */
292
+ export const bindEditor = bindEditorImpl;
293
+
294
+ function bindEditorToSubtree<TSchema extends ImplicitFieldSchema>(
295
+ tree: Subtree<TSchema>,
296
+ executeEdit: SynchronousEditor | AsynchronousEditor,
297
+ ): (code: string) => void | Promise<void> {
298
+ // Stick the tree schema constructors on an object passed to the function so that the LLM can create new nodes.
299
+ const create: Record<string, (input: FactoryContentObject) => TreeNode> = {};
300
+ const is: Record<string, <T extends TreeNode>(input: unknown) => input is T> = {};
301
+ for (const schema of findNamedSchemas(tree.schema)) {
302
+ const name = getFriendlyName(schema);
303
+ create[name] = (input: FactoryContentObject) => constructTreeNode(schema, input);
304
+ is[name] = <T extends TreeNode>(input: unknown): input is T => Tree.is(input, schema);
305
+ }
306
+
307
+ const context = {
308
+ get root(): ReadableField<TSchema> {
309
+ return tree.field;
310
+ },
311
+ set root(value: TreeFieldFromImplicitField<ReadSchema<TSchema>>) {
312
+ tree.field = value;
313
+ },
314
+ create,
315
+ is,
316
+ parent: (child: TreeNode): TreeNode | undefined => Tree.parent(child),
317
+ key: (child: TreeNode): string | number => Tree.key(child),
318
+ } satisfies Context<TSchema>;
319
+
320
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
321
+ return (code: string) => executeEdit(context, code);
322
+ }
package/src/api.ts CHANGED
@@ -4,9 +4,13 @@
4
4
  */
5
5
 
6
6
  import type { ImplicitFieldSchema, TreeNode } from "@fluidframework/tree";
7
- // This is used for doc links
7
+ // These are used for doc links
8
8
  import type { FactoryContentObject, ReadableField } from "@fluidframework/tree/alpha";
9
9
 
10
+ // This is used for doc links
11
+ // eslint-disable-next-line unused-imports/no-unused-imports
12
+ import type { bindEditor, defaultEditor } from "./agent.js";
13
+
10
14
  /**
11
15
  * Logger interface for logging events from a {@link SharedTreeSemanticAgent}.
12
16
  * @alpha
@@ -18,6 +22,91 @@ export interface Logger {
18
22
  log(message: string): void;
19
23
  }
20
24
 
25
+ /**
26
+ * The context object available to generated code when editing a tree.
27
+ * @remarks This object is provided to JavaScript code executed by the {@link SynchronousEditor | editor} as a variable named `context`.
28
+ * It contains the current state of the tree and utilities for creating and inspecting tree nodes.
29
+ * @alpha
30
+ */
31
+ export interface Context<TSchema extends ImplicitFieldSchema> {
32
+ /**
33
+ * The root of the tree that can be read or modified.
34
+ * @remarks
35
+ * You can read properties and navigate through the tree starting from this root.
36
+ * You can also assign a new value to this property to replace the entire tree, as long as the new value is one of the types allowed at the root.
37
+ *
38
+ * Example: Read the current root with `const currentRoot = context.root;`
39
+ *
40
+ * Example: Replace the entire root with `context.root = context.create.MyRootType({ });`
41
+ */
42
+ root: ReadableField<TSchema>;
43
+
44
+ /**
45
+ * A collection of builder functions for creating new tree nodes.
46
+ * @remarks
47
+ * Each property on this object is named after a type in the tree schema.
48
+ * Call the corresponding function to create a new node of that type.
49
+ * Always use these builder functions when creating new nodes rather than plain JavaScript objects.
50
+ *
51
+ * Example: Create a new Person node with `context.create.Person({ name: "Alice", age: 30 })`
52
+ *
53
+ * Example: Create a new Task node with `context.create.Task({ title: "Buy groceries", completed: false })`
54
+ */
55
+ create: Record<string, (input: FactoryContentObject) => TreeNode>;
56
+
57
+ /**
58
+ * A collection of type-checking functions for tree nodes.
59
+ * @remarks
60
+ * Each property on this object is named after a type in the tree schema.
61
+ * Call the corresponding function to check if a node is of that specific type.
62
+ * This is useful when working with nodes that could be one of multiple types.
63
+ *
64
+ * Example: Check if a node is a Person with `if (context.is.Person(node)) { console.log(node.name); }`
65
+ */
66
+ is: Record<string, <T extends TreeNode>(input: T) => input is T>;
67
+
68
+ /**
69
+ * Returns the parent node of the given child node.
70
+ * @param child - The node whose parent you want to find.
71
+ * @returns The parent node, or `undefined` if the node is the root or is not in the tree.
72
+ * @remarks
73
+ * Example: Get the parent with `const parent = context.parent(childNode);`
74
+ */
75
+ parent(child: TreeNode): TreeNode | undefined;
76
+
77
+ /**
78
+ * Returns the key or index of the given node within its parent.
79
+ * @param child - The node whose key you want to find.
80
+ * @returns A string key if the node is in an object or map, or a numeric index if the node is in an array.
81
+ * @remarks
82
+ * For a node in an object, this might return a string like "firstName".
83
+ * For a node in an array, this might return a number like 0, 1, 2, etc.
84
+ *
85
+ * Example: `const key = context.key(childNode);`
86
+ */
87
+ key(child: TreeNode): string | number;
88
+ }
89
+
90
+ /**
91
+ * A synchronous function that executes a string of JavaScript code to perform an edit within a {@link SharedTreeSemanticAgent}.
92
+ * @param context - An object that must be provided to the generated code as a variable named "context" in its top-level scope.
93
+ * @param code - The JavaScript code that should be executed.
94
+ * @remarks To simulate the execution of an editor outside of an {@link SharedTreeSemanticAgent | agent}, you can use {@link bindEditor | bindEditor} to bind an editor to a specific subtree.
95
+ * @alpha
96
+ */
97
+ export type SynchronousEditor = (context: Record<string, unknown>, code: string) => void;
98
+ /**
99
+ * An asynchronous function that executes a string of JavaScript code to perform an edit within a {@link SharedTreeSemanticAgent}.
100
+ * @param context - An object that must be provided to the generated code as a variable named "context" in its top-level scope.
101
+ * @param code - The JavaScript code that should be executed.
102
+ * @remarks To simulate the execution of an editor outside of an {@link SharedTreeSemanticAgent | agent}, you can use {@link bindEditor | bindEditor} to bind an editor to a specific subtree.
103
+ * @alpha
104
+ */
105
+ export type AsynchronousEditor = (
106
+ context: Record<string, unknown>,
107
+ code: string,
108
+ ) => Promise<void>;
109
+
21
110
  /**
22
111
  * Options used to parameterize the creation of a {@link SharedTreeSemanticAgent}.
23
112
  * @alpha
@@ -28,22 +117,14 @@ export interface SemanticAgentOptions {
28
117
  */
29
118
  domainHints?: string;
30
119
  /**
31
- * Validates any generated JavaScript created by the {@link SharedTreeChatModel.editToolName | model's editing tool}.
32
- * @remarks This happens before the code is executed - execution can be intercepted by using the {@link SemanticAgentOptions.executeEdit | executeEdit} callback.
33
- * @param code - The generated JavaScript code as a string.
34
- * @throws If the code is invalid, this function should throw an error with a human-readable message describing why it is invalid.
35
- */
36
- validateEdit?: (code: string) => void | Promise<void>;
37
- /**
38
- * Evaluates/runs any generated JavaScript created by the {@link SharedTreeChatModel.editToolName | model's editing tool}.
39
- * @remarks This happens only after the code has been successfully validated by the optional {@link SemanticAgentOptions.validateEdit | validateEdit} function.
40
- * @param context - An object that must be provided to the generated code as a variable named "context" in its top-level scope.
41
- * @param code - The generated JavaScript code as a string.
42
- * @throws If an error is thrown while executing the code, it will be caught and the message will be forwarded to the model for debugging.
43
- * @remarks If this function is not provided, the generated code will be executed using a simple `eval` call, which may not provide sufficient security guarantees for some environments.
120
+ * Executes any generated JavaScript created by the {@link SharedTreeChatModel.editToolName | model's editing tool}.
121
+ * @remarks If an error is thrown while executing the code, it will be caught and the message will be forwarded to the {@link SharedTreeChatModel | model} for debugging.
122
+ * @remarks If this function is not provided, the generated code will be executed using a {@link defaultEditor | simple default} which may not provide sufficient security guarantees for some environments.
44
123
  * Use a library such as SES to provide a more secure implementation - see `@fluidframework/tree-agent-ses` for a drop-in implementation.
124
+ *
125
+ * To simulate the execution of an editor outside of an {@link SharedTreeSemanticAgent | agent}, you can use {@link bindEditor | bindEditor} to bind an editor to a specific subtree.
45
126
  */
46
- executeEdit?: (context: Record<string, unknown>, code: string) => void | Promise<void>;
127
+ editor?: SynchronousEditor | AsynchronousEditor;
47
128
  /**
48
129
  * The maximum number of sequential edits the LLM can make before we assume it's stuck in a loop.
49
130
  */
@@ -64,18 +145,11 @@ export interface EditResult {
64
145
  * @remarks
65
146
  * - `success`: The edit was successfully applied.
66
147
  * - `disabledError`: The model is not allowed to edit the tree (i.e. {@link SharedTreeChatModel.editToolName} was not provided).
67
- * - `validationError`: The provided JavaScript did not pass the optional {@link SemanticAgentOptions.validateEdit} function.
68
- * - `executionError`: An error was thrown while parsing or executing the provided JavaScript.
148
+ * - `editingError`: An error was thrown while parsing or executing the provided JavaScript.
69
149
  * - `tooManyEditsError`: The {@link SharedTreeChatQuery.edit} function has been called more than the number of times specified by {@link SemanticAgentOptions.maximumSequentialEdits} for the same message.
70
150
  * - `expiredError`: The {@link SharedTreeChatQuery.edit} function was called after the issuing query has already completed.
71
151
  */
72
- type:
73
- | "success"
74
- | "disabledError"
75
- | "validationError"
76
- | "executionError"
77
- | "tooManyEditsError"
78
- | "expiredError";
152
+ type: "success" | "disabledError" | "editingError" | "tooManyEditsError" | "expiredError";
79
153
 
80
154
  /**
81
155
  * A human-readable message describing the result of the edit attempt.
package/src/index.ts CHANGED
@@ -9,13 +9,20 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
 
12
- export { SharedTreeSemanticAgent } from "./agent.js";
12
+ export {
13
+ SharedTreeSemanticAgent,
14
+ bindEditor,
15
+ bindEditorImpl,
16
+ defaultEditor,
17
+ } from "./agent.js";
13
18
  export type {
14
19
  EditResult,
15
20
  SharedTreeChatModel,
16
21
  SharedTreeChatQuery,
17
22
  Logger,
18
23
  SemanticAgentOptions,
24
+ SynchronousEditor,
25
+ AsynchronousEditor,
19
26
  } from "./api.js";
20
27
  export { type TreeView, llmDefault } from "./utils.js";
21
28
  export {
package/src/prompt.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { oob } from "@fluidframework/core-utils/internal";
6
7
  import { NodeKind, Tree, TreeNode } from "@fluidframework/tree";
7
8
  import type { ImplicitFieldSchema, TreeMapNode } from "@fluidframework/tree";
8
9
  import type { ReadableField } from "@fluidframework/tree/alpha";
@@ -34,10 +35,20 @@ export function getPrompt<TRoot extends ImplicitFieldSchema>(args: {
34
35
  const mapInterfaceName = "TreeMap";
35
36
  const simpleSchema = getSimpleSchema(schema);
36
37
  // Inspect the schema to determine what kinds of nodes are possible - this will affect how much information we need to include in the prompt.
38
+ const rootTypes = [...normalizeFieldSchema(schema).allowedTypeSet];
39
+ const rootTypeUnion = `${rootTypes.map((t) => getFriendlyName(t)).join(" | ")}`;
40
+ let nodeTypeUnion: string | undefined;
37
41
  let hasArrays = false;
38
42
  let hasMaps = false;
39
43
  let exampleObjectName: string | undefined;
40
44
  for (const [definition, nodeSchema] of simpleSchema.definitions) {
45
+ if (nodeSchema.kind !== NodeKind.Leaf) {
46
+ nodeTypeUnion =
47
+ nodeTypeUnion === undefined
48
+ ? unqualifySchema(definition)
49
+ : `${nodeTypeUnion} | ${unqualifySchema(definition)}`;
50
+ }
51
+
41
52
  switch (nodeSchema.kind) {
42
53
  case NodeKind.Array: {
43
54
  hasArrays = true;
@@ -68,24 +79,81 @@ export function getPrompt<TRoot extends ImplicitFieldSchema>(args: {
68
79
  const stringified = stringifyTree(field);
69
80
  const details: SchemaDetails = { hasHelperMethods: false };
70
81
  const typescriptSchemaTypes = getZodSchemaAsTypeScript(domainTypes, details);
71
- const helperMethodExplanation = details.hasHelperMethods
72
- ? `Manipulating the data using the APIs described below is allowed, but when possible ALWAYS prefer to use any application helper methods exposed on the schema TypeScript types if the goal can be accomplished that way.
73
- It will often not be possible to fully accomplish the goal using those helpers. When this is the case, mutate the objects as normal, taking into account the following guidance.`
74
- : "";
75
82
 
76
- const builderExplanation =
83
+ const create =
84
+ exampleObjectName === undefined
85
+ ? ""
86
+ : `\n /**
87
+ * A collection of builder functions for creating new tree nodes.
88
+ * @remarks
89
+ * Each property on this object is named after a type in the tree schema.
90
+ * Call the corresponding function to create a new node of that type.
91
+ * Always use these builder functions when creating new nodes rather than plain JavaScript objects.
92
+ *
93
+ * For example:
94
+ *
95
+ * \`\`\`javascript
96
+ * // This creates a new ${exampleObjectName} object:
97
+ * const ${communize(exampleObjectName)} = context.create.${exampleObjectName}({ ...properties });
98
+ * // Don't do this:
99
+ * // const ${communize(exampleObjectName)} = { ...properties };
100
+ * \`\`\`
101
+ */
102
+ create: Record<string, <T extends TreeData>(input: T) => T>;\n`;
103
+
104
+ const isDocs =
77
105
  exampleObjectName === undefined
78
106
  ? ""
79
- : `When constructing new objects, you should wrap them in the appropriate builder function rather than simply making a javascript object.
80
- The builders are available on the \`create\` property on the context object and are named according to the type that they create.
81
- For example:
107
+ : `\n /**
108
+ * A collection of type-guard functions for data in the tree.
109
+ * @remarks
110
+ * Each property on this object is named after a type in the tree schema.
111
+ * Call the corresponding function to check if a node is of that specific type.
112
+ * This is useful when working with nodes that could be one of multiple types.
113
+ *
114
+ * ${`Example: Check if a node is a ${exampleObjectName} with \`if (context.is.${exampleObjectName}(node)) {}\``}
115
+ */
116
+ is: Record<string, <T extends TreeData>(input: unknown) => input is T>;\n`;
82
117
 
83
- \`\`\`javascript
84
- // This creates a new ${exampleObjectName} object:
85
- const ${communize(exampleObjectName)} = context.create.${exampleObjectName}({ /* ...properties... */ });
86
- // Don't do this:
87
- // const ${communize(exampleObjectName)} = { /* ...properties... */ };
88
- \`\`\`\n\n`;
118
+ const context = `\`\`\`typescript
119
+ ${nodeTypeUnion === undefined ? "" : `type TreeData = ${nodeTypeUnion};\n\n`} /**
120
+ * An object available to generated code which provides read and write access to the tree as well as utilities for creating and inspecting data in the tree.
121
+ * @remarks This object is available as a variable named \`context\` in the scope of the generated JavaScript snippet.
122
+ */
123
+ interface Context<TSchema extends ImplicitFieldSchema> {
124
+ /**
125
+ * The root of the tree that can be read or mutated.
126
+ * @remarks
127
+ * You can read properties and navigate through the tree starting from this root.
128
+ * You can also assign a new value to this property to replace the entire tree, as long as the new value is one of the types allowed at the root.
129
+ *
130
+ * Example: Read the current root with \`const currentRoot = context.root;\`
131
+ *${rootTypes.length > 0 ? ` Example: Replace the entire root with \`context.root = context.create.${getFriendlyName(rootTypes[0] ?? oob())}({ });\`\n *` : ""}/
132
+ root: ReadableField<TSchema>;
133
+ ${create}
134
+ ${isDocs}
135
+ /**
136
+ * Returns the parent object/array/map of the given object/array/map, if there is one.
137
+ * @returns The parent node, or \`undefined\` if the node is the root or is not in the tree.
138
+ * @remarks
139
+ * Example: Get the parent with \`const parent = context.parent(child);\`
140
+ */
141
+ parent(child: TreeData): TreeData | undefined;
142
+
143
+ /**
144
+ * Returns the property key or index of the given object/array/map within its parent.
145
+ * @returns A string key if the child is in an object or map, or a numeric index if the child is in an array.
146
+ *
147
+ * Example: \`const key = context.key(child);\`
148
+ */
149
+ key(child: TreeData): string | number;
150
+ }
151
+ \`\`\``;
152
+
153
+ const helperMethodExplanation = details.hasHelperMethods
154
+ ? `Manipulating the data using the APIs described below is allowed, but when possible ALWAYS prefer to use any application helper methods exposed on the schema TypeScript types if the goal can be accomplished that way.
155
+ It will often not be possible to fully accomplish the goal using those helpers. When this is the case, mutate the objects as normal, taking into account the following guidance.`
156
+ : "";
89
157
 
90
158
  const reinsertionExplanation = `Once non-primitive data has been removed from the tree (e.g. replaced via assignment, or removed from an array), that data cannot be re-inserted into the tree.
91
159
  Instead, it must be deep cloned and recreated.
@@ -160,7 +228,6 @@ ${getTreeMapNodeDocumentation(mapInterfaceName)}
160
228
 
161
229
  `;
162
230
 
163
- const rootTypes = normalizeFieldSchema(schema).allowedTypeSet;
164
231
  const editing = `If the user asks you to edit the tree, you should author a snippet of JavaScript code to accomplish the user-specified goal, following the instructions for editing detailed below.
165
232
  You must use the "${editToolName}" tool to run the generated code.
166
233
  After editing the tree, review the latest state of the tree to see if it satisfies the user's request.
@@ -173,14 +240,18 @@ If the user asks you to edit the document, you will write a snippet of JavaScrip
173
240
  The snippet may be synchronous or asynchronous (i.e. it may \`await\` functions if necessary).
174
241
  The snippet has a \`context\` variable in its scope.
175
242
  This \`context\` variable holds the current state of the tree in the \`root\` property.
176
- You may mutate any part of the root tree as necessary, taking into account the caveats around${hasArrays ? ` arrays${hasMaps ? " and" : ""}` : ""}${hasMaps ? " maps" : ""} detailed below.
177
- You may also set the \`root\` property of the context to be an entirely new value as long as it is one of the types allowed at the root of the tree (\`${Array.from(rootTypes.values(), (t) => getFriendlyName(t)).join(" | ")}\`).
243
+ You may mutate any part of this tree as necessary, taking into account the caveats around${hasArrays ? ` arrays${hasMaps ? " and" : ""}` : ""}${hasMaps ? " maps" : ""} detailed below.
244
+ You may also set the \`root\` property of the context to be an entirely new value as long as it is one of the types allowed at the root of the tree (\`${rootTypeUnion}\`).
245
+ You should also use the \`context\` object to create new data to insert into the tree, using the builder functions available on the \`create\` property.
246
+ There are other additional helper functions available on the \`context\` object to help you analyze the tree.
247
+ Here is the definition of the \`Context\` interface:
248
+ ${context}
178
249
  ${helperMethodExplanation}
179
250
  ${hasArrays ? arrayEditing : ""}${hasMaps ? mapEditing : ""}#### Additional Notes
180
251
 
181
252
  Before outputting the edit function, you should check that it is valid according to both the application tree's schema and any restrictions of the editing APIs described above.
182
253
 
183
- ${builderExplanation}${reinsertionExplanation}
254
+ ${reinsertionExplanation}
184
255
 
185
256
  Finally, double check that the edits would accomplish the user's request (if it is possible).
186
257