@fluidframework/ai-collab 2.40.0-336023 → 2.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +156 -0
  3. package/api-report/ai-collab.alpha.api.md +88 -0
  4. package/dist/aiCollabApi.d.ts +9 -0
  5. package/dist/aiCollabApi.d.ts.map +1 -1
  6. package/dist/aiCollabApi.js.map +1 -1
  7. package/dist/alpha.d.ts +12 -0
  8. package/dist/diffTypes.d.ts +200 -0
  9. package/dist/diffTypes.d.ts.map +1 -0
  10. package/dist/diffTypes.js +7 -0
  11. package/dist/diffTypes.js.map +1 -0
  12. package/dist/explicit-strategy/agentEditReducer.d.ts +25 -3
  13. package/dist/explicit-strategy/agentEditReducer.d.ts.map +1 -1
  14. package/dist/explicit-strategy/agentEditReducer.js +239 -15
  15. package/dist/explicit-strategy/agentEditReducer.js.map +1 -1
  16. package/dist/explicit-strategy/index.d.ts +3 -0
  17. package/dist/explicit-strategy/index.d.ts.map +1 -1
  18. package/dist/explicit-strategy/index.js +6 -2
  19. package/dist/explicit-strategy/index.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/lib/aiCollabApi.d.ts +9 -0
  24. package/lib/aiCollabApi.d.ts.map +1 -1
  25. package/lib/aiCollabApi.js.map +1 -1
  26. package/lib/alpha.d.ts +12 -0
  27. package/lib/diffTypes.d.ts +200 -0
  28. package/lib/diffTypes.d.ts.map +1 -0
  29. package/lib/diffTypes.js +6 -0
  30. package/lib/diffTypes.js.map +1 -0
  31. package/lib/explicit-strategy/agentEditReducer.d.ts +25 -3
  32. package/lib/explicit-strategy/agentEditReducer.d.ts.map +1 -1
  33. package/lib/explicit-strategy/agentEditReducer.js +239 -18
  34. package/lib/explicit-strategy/agentEditReducer.js.map +1 -1
  35. package/lib/explicit-strategy/index.d.ts +3 -0
  36. package/lib/explicit-strategy/index.d.ts.map +1 -1
  37. package/lib/explicit-strategy/index.js +6 -2
  38. package/lib/explicit-strategy/index.js.map +1 -1
  39. package/lib/index.d.ts +1 -0
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js.map +1 -1
  42. package/package.json +9 -9
  43. package/src/aiCollabApi.ts +10 -0
  44. package/src/diffTypes.ts +211 -0
  45. package/src/explicit-strategy/agentEditReducer.ts +296 -19
  46. package/src/explicit-strategy/index.ts +10 -2
  47. package/src/index.ts +15 -0
@@ -23,6 +23,18 @@ import {
23
23
  } from "@fluidframework/tree/internal";
24
24
  import { closest } from "fastest-levenshtein";
25
25
 
26
+ import type {
27
+ ArrayRangeRemoveDiff,
28
+ ArraySingleRemoveDiff,
29
+ InsertDiff,
30
+ ModifyDiff,
31
+ MoveRangeDiff,
32
+ MoveSingleDiff,
33
+ NodePath,
34
+ RemoveNodeDiff,
35
+ Diff,
36
+ } from "../diffTypes.js";
37
+
26
38
  import {
27
39
  type TreeEdit,
28
40
  type ObjectTarget,
@@ -34,6 +46,9 @@ import {
34
46
  type TreeEditValue,
35
47
  typeField,
36
48
  type Modify,
49
+ type Remove,
50
+ type Move,
51
+ objectIdKey,
37
52
  } from "./agentEditTypes.js";
38
53
  import type { IdGenerator } from "./idGenerator.js";
39
54
  import type { JsonValue } from "./jsonTypes.js";
@@ -63,7 +78,10 @@ function populateDefaults(
63
78
  }
64
79
  }
65
80
 
66
- function getSchemaIdentifier(content: TreeEditValue): string | undefined {
81
+ /**
82
+ * Gets the schema identifier of the given content, including primitive values.
83
+ */
84
+ export function getSchemaIdentifier(content: TreeEditValue): string {
67
85
  switch (typeof content) {
68
86
  case "boolean": {
69
87
  return SchemaFactory.boolean.identifier;
@@ -84,6 +102,7 @@ function getSchemaIdentifier(content: TreeEditValue): string | undefined {
84
102
  if (isFluidHandle(content)) {
85
103
  return SchemaFactory.handle.identifier;
86
104
  }
105
+
87
106
  return content[typeField];
88
107
  }
89
108
  default: {
@@ -92,7 +111,10 @@ function getSchemaIdentifier(content: TreeEditValue): string | undefined {
92
111
  }
93
112
  }
94
113
 
95
- function contentWithIds(content: TreeNode, idGenerator: IdGenerator): TreeEditObject {
114
+ /**
115
+ * Converts a tree node from a {@link TreeEdit} to a {@link TreeEditObject} with the proper object IDs.
116
+ */
117
+ export function contentWithIds(content: TreeNode, idGenerator: IdGenerator): TreeEditObject {
96
118
  return JSON.parse(toDecoratedJson(idGenerator, content)) as TreeEditObject;
97
119
  }
98
120
 
@@ -104,7 +126,7 @@ export function applyAgentEdit(
104
126
  idGenerator: IdGenerator,
105
127
  definitionMap: ReadonlyMap<string, SimpleNodeSchema>,
106
128
  validator?: (edit: TreeNode) => void,
107
- ): TreeEdit {
129
+ ): { edit: TreeEdit; diff: Diff } {
108
130
  assertObjectIdsExist(treeEdit, idGenerator);
109
131
  switch (treeEdit.type) {
110
132
  case "insert": {
@@ -125,10 +147,14 @@ export function applyAgentEdit(
125
147
  const simpleNodeSchema = allowedType as unknown as new (dummy: unknown) => TreeNode;
126
148
  const insertNode = new simpleNodeSchema(treeEdit.content);
127
149
  validator?.(insertNode);
150
+
128
151
  array.insertAt(index, insertNode as unknown as IterableTreeArrayContent<never>);
129
152
  return {
130
- ...treeEdit,
131
- content: contentWithIds(insertNode, idGenerator),
153
+ edit: {
154
+ ...treeEdit,
155
+ content: contentWithIds(insertNode, idGenerator),
156
+ },
157
+ diff: createInsertDiff(insertNode, treeEdit.explanation, idGenerator),
132
158
  };
133
159
  }
134
160
  }
@@ -136,6 +162,7 @@ export function applyAgentEdit(
136
162
  }
137
163
  case "remove": {
138
164
  const source = treeEdit.source;
165
+ let diff: RemoveNodeDiff | ArraySingleRemoveDiff | ArrayRangeRemoveDiff;
139
166
  if (isObjectTarget(source)) {
140
167
  const node = getNodeFromTarget(source, idGenerator);
141
168
  const parentNode = Tree.parent(node);
@@ -146,7 +173,9 @@ export function applyAgentEdit(
146
173
  );
147
174
  } else if (Tree.schema(parentNode).kind === NodeKind.Array) {
148
175
  const nodeIndex = Tree.key(node) as number;
149
- (parentNode as TreeArrayNode).removeAt(nodeIndex);
176
+ const parentArrayNode = parentNode as TreeArrayNode;
177
+ diff = createRemoveDiff(treeEdit, idGenerator);
178
+ parentArrayNode.removeAt(nodeIndex);
150
179
  } else {
151
180
  const fieldKey = Tree.key(node);
152
181
  const parentSchema = Tree.schema(parentNode);
@@ -154,6 +183,7 @@ export function applyAgentEdit(
154
183
  (parentSchema.info as Record<string, ImplicitFieldSchema>)[fieldKey] ??
155
184
  fail("Expected field schema");
156
185
  if (fieldSchema instanceof FieldSchema && fieldSchema.kind === FieldKind.Optional) {
186
+ diff = createRemoveDiff(treeEdit, idGenerator);
157
187
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
158
188
  (parentNode as any)[fieldKey] = undefined;
159
189
  } else {
@@ -164,9 +194,13 @@ export function applyAgentEdit(
164
194
  }
165
195
  } else if (isRange(source)) {
166
196
  const { array, startIndex, endIndex } = getRangeInfo(source, idGenerator);
197
+ diff = createRemoveDiff(treeEdit, idGenerator);
167
198
  array.removeRange(startIndex, endIndex);
199
+ } else {
200
+ throw new UsageError("Invalid source for remove edit");
168
201
  }
169
- return treeEdit;
202
+
203
+ return { edit: treeEdit, diff };
170
204
  }
171
205
  case "modify": {
172
206
  const node = getNodeFromTarget(treeEdit.target, idGenerator);
@@ -187,11 +221,11 @@ export function applyAgentEdit(
187
221
  }
188
222
 
189
223
  const modification = treeEdit.modification;
190
-
191
224
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
192
225
  const schemaIdentifier = (modification as any)[typeField];
193
226
 
194
227
  let insertedObject: TreeNode | undefined;
228
+ const diff = createModifyDiff(treeEdit, idGenerator);
195
229
  // if fieldSchema is a LeafnodeSchema, we can check that it's a valid type and set the field.
196
230
  if (isPrimitive(modification)) {
197
231
  try {
@@ -224,6 +258,7 @@ export function applyAgentEdit(
224
258
  populateDefaults(modification, definitionMap);
225
259
  const constructedModification = new simpleSchema(modification);
226
260
  validator?.(constructedModification);
261
+
227
262
  insertedObject = constructedModification;
228
263
 
229
264
  if (Array.isArray(modification)) {
@@ -235,12 +270,9 @@ export function applyAgentEdit(
235
270
  0xa76 /* the modification must be an array node */,
236
271
  );
237
272
  field.removeRange(0);
238
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
239
- (node as any)[treeEdit.field] = constructedModification;
240
- } else {
241
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
242
- (node as any)[treeEdit.field] = constructedModification;
243
273
  }
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
275
+ (node as any)[treeEdit.field] = constructedModification;
244
276
  }
245
277
  // If the fieldSchema is of type FieldSchema, we can check its allowed types and set the field.
246
278
  else if (fieldSchema instanceof FieldSchema) {
@@ -266,11 +298,15 @@ export function applyAgentEdit(
266
298
  }
267
299
  }
268
300
  }
301
+
269
302
  return insertedObject === undefined
270
- ? treeEdit
303
+ ? { edit: treeEdit, diff }
271
304
  : {
272
- ...treeEdit,
273
- modification: contentWithIds(insertedObject, idGenerator),
305
+ edit: {
306
+ ...treeEdit,
307
+ modification: contentWithIds(insertedObject, idGenerator),
308
+ },
309
+ diff,
274
310
  };
275
311
  }
276
312
  case "move": {
@@ -281,7 +317,7 @@ export function applyAgentEdit(
281
317
  destination,
282
318
  idGenerator,
283
319
  );
284
-
320
+ const diff: MoveSingleDiff | MoveRangeDiff = createMoveDiff(treeEdit, idGenerator);
285
321
  if (isObjectTarget(source)) {
286
322
  const sourceNode = getNodeFromTarget(source, idGenerator);
287
323
  const sourceIndex = Tree.key(sourceNode) as number;
@@ -329,8 +365,10 @@ export function applyAgentEdit(
329
365
  sourceEndIndex,
330
366
  array,
331
367
  );
368
+ } else {
369
+ throw new Error("Invalid source for move edit");
332
370
  }
333
- return treeEdit;
371
+ return { edit: treeEdit, diff };
334
372
  }
335
373
  default: {
336
374
  fail("invalid tree edit");
@@ -408,7 +446,10 @@ interface RangeInfo {
408
446
  endIndex: number;
409
447
  }
410
448
 
411
- function getRangeInfo(range: Range, idGenerator: IdGenerator): RangeInfo {
449
+ /**
450
+ * Gets information about the range of nodes being targeted by an {@link Range}
451
+ */
452
+ export function getRangeInfo(range: Range, idGenerator: IdGenerator): RangeInfo {
412
453
  const { array: arrayFrom, index: startIndex } = getPlaceInfo(range.from, idGenerator);
413
454
  const { array: arrayTo, index: endIndex } = getPlaceInfo(range.to, idGenerator);
414
455
 
@@ -552,6 +593,242 @@ function assertObjectIdsExist(treeEdit: TreeEdit, idGenerator: IdGenerator): voi
552
593
  }
553
594
  }
554
595
 
596
+ const createNodePathRecursive = (
597
+ node: TreeNode | undefined,
598
+ idGenerator: IdGenerator,
599
+ currentPath: NodePath,
600
+ ): NodePath => {
601
+ if (node === undefined) {
602
+ return currentPath;
603
+ }
604
+
605
+ currentPath.push({
606
+ shortId: Tree.shortId(node),
607
+ schemaIdentifier: Tree.schema(node).identifier,
608
+ parentField: Tree.key(node),
609
+ });
610
+
611
+ const parentNode = Tree.parent(node);
612
+ return createNodePathRecursive(parentNode, idGenerator, currentPath);
613
+ };
614
+
615
+ /**
616
+ * Creates a diff for an Insert TreeEdit.
617
+ *
618
+ * @remarks
619
+ * This function is only invoked within the "insert" case block.
620
+ *
621
+ * This must only be called AFTER an insertion is made.
622
+ * It generates the insert diff after the node has been successfully inserted, as the node's index may
623
+ * be required to support undoing the insert operation, and we don't know that index until the insert has been made.
624
+ */
625
+ function createInsertDiff(
626
+ newlyInsertedNode: TreeNode,
627
+ aiExplanation: string,
628
+ idGenerator: IdGenerator,
629
+ ): InsertDiff {
630
+ return {
631
+ type: "insert",
632
+ nodePath: createNodePathRecursive(newlyInsertedNode, idGenerator, []),
633
+ nodeContent: JSON.parse(JSON.stringify(newlyInsertedNode)),
634
+ aiExplanation,
635
+ };
636
+ }
637
+
638
+ /**
639
+ * Returns an object identical to the input except that the special 'objectIdKey' field (only intended for use by the LLM agent) is removed if present.
640
+ * @remarks The input object is not modified.
641
+ */
642
+ function removeAgentObjectIdField(oldValue: unknown): unknown {
643
+ if (typeof oldValue === "object" && oldValue !== null && !Array.isArray(oldValue)) {
644
+ const { [objectIdKey]: _, ...rest } = oldValue as Record<string, unknown>;
645
+ return rest;
646
+ }
647
+ return oldValue;
648
+ }
649
+
650
+ /**
651
+ * Creates a diff for a Modify TreeEdit.
652
+ *
653
+ * @remarks
654
+ * This function must only be called BEFORE a modify edit is applied.
655
+ * For move operations, the diff is created before the node(s) have been successfully moved,
656
+ * since the original index is needed to restore the node(s) if the move operation need to undo.
657
+ */
658
+ function createModifyDiff(treeEdit: Modify, idGenerator: IdGenerator): ModifyDiff {
659
+ const targetNode = getNodeFromTarget(treeEdit.target, idGenerator);
660
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
661
+ const targetNodeAtField: unknown = (targetNode as any)[treeEdit.field];
662
+
663
+ if (isPrimitive(targetNodeAtField)) {
664
+ return {
665
+ type: "modify",
666
+ nodePath: createNodePathRecursive(targetNode, idGenerator, [
667
+ {
668
+ shortId: undefined,
669
+ parentField: treeEdit.field,
670
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
671
+ schemaIdentifier: getSchemaIdentifier(treeEdit.modification)!,
672
+ },
673
+ ]),
674
+ newValue: treeEdit.modification,
675
+ oldValue: targetNodeAtField,
676
+ aiExplanation: treeEdit.explanation,
677
+ };
678
+ }
679
+
680
+ return {
681
+ type: "modify",
682
+ nodePath: createNodePathRecursive(targetNodeAtField as TreeNode, idGenerator, []),
683
+ newValue: treeEdit.modification,
684
+ oldValue: removeAgentObjectIdField(JSON.parse(JSON.stringify(targetNodeAtField))),
685
+ aiExplanation: treeEdit.explanation,
686
+ };
687
+ }
688
+
689
+ /**
690
+ * Creates a diff for a Remove TreeEdit.
691
+ *
692
+ * @remarks
693
+ * This function must only be called BEFORE a remove edit is applied.
694
+ * It generates the remove diff before the node has been successfully removed, as the node's index may
695
+ * be required to support undoing the remove operation, and we don't know that index until the remove has been made.
696
+ */
697
+ function createRemoveDiff(
698
+ treeEdit: Remove,
699
+ idGenerator: IdGenerator,
700
+ ): RemoveNodeDiff | ArraySingleRemoveDiff | ArrayRangeRemoveDiff {
701
+ const source = treeEdit.source;
702
+ if (isObjectTarget(source)) {
703
+ const node = getNodeFromTarget(source, idGenerator);
704
+ const parentNode = Tree.parent(node);
705
+ if (parentNode === undefined) {
706
+ throw new Error("Unexpectedly received a root node as the target of a remove edit");
707
+ } else if (Tree.schema(parentNode).kind === NodeKind.Array) {
708
+ const nodeIndex = Tree.key(node) as number;
709
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
710
+ const targetRemovedNode = (parentNode as TreeArrayNode).at(nodeIndex)!;
711
+
712
+ if (isPrimitive(targetRemovedNode)) {
713
+ // Note that this cause should not be possible, still putting the error here in case things change so that this function is updated properly
714
+ throw new Error(
715
+ "Unexpectedly recieved a primitive node as the target of a remove edit",
716
+ );
717
+ }
718
+
719
+ return {
720
+ type: "remove",
721
+ removalType: "remove-array-single",
722
+ nodePath: createNodePathRecursive(targetRemovedNode as TreeNode, idGenerator, []),
723
+ aiExplanation: treeEdit.explanation,
724
+ nodeContent: removeAgentObjectIdField(JSON.parse(JSON.stringify(targetRemovedNode))),
725
+ };
726
+ } else {
727
+ const fieldKey = Tree.key(node);
728
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
729
+ const targetNodeAtField: unknown = (parentNode as any)[fieldKey];
730
+
731
+ if (isPrimitive(targetNodeAtField)) {
732
+ // Note that this cause should not be possible, still putting the error here in case things change so that this function is updated properly
733
+ throw new Error(
734
+ "Unexpectedly recieved a primitive node as the target of a remove field edit",
735
+ );
736
+ }
737
+
738
+ return {
739
+ type: "remove",
740
+ removalType: "remove-node",
741
+ nodePath: createNodePathRecursive(targetNodeAtField as TreeNode, idGenerator, []),
742
+ aiExplanation: treeEdit.explanation,
743
+ nodeContent: removeAgentObjectIdField(JSON.parse(JSON.stringify(targetNodeAtField))),
744
+ };
745
+ }
746
+ } else if (isRange(source)) {
747
+ const { array, startIndex, endIndex } = getRangeInfo(source, idGenerator);
748
+ const removedNodePaths: NodePath[] = [];
749
+ const removedNodes: TreeNode[] = [];
750
+ for (let i = startIndex; i < endIndex; i++) {
751
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
752
+ const nodeToRemove = array.at(i)!;
753
+ if (!isPrimitive(nodeToRemove)) {
754
+ removedNodePaths.push(
755
+ createNodePathRecursive(nodeToRemove as TreeNode, idGenerator, []),
756
+ );
757
+ removedNodes.push(nodeToRemove as TreeNode);
758
+ }
759
+ }
760
+ return {
761
+ type: "remove",
762
+ removalType: "remove-array-range",
763
+ nodePaths: removedNodePaths,
764
+ aiExplanation: treeEdit.explanation,
765
+ nodeContents: removedNodes.map((node) =>
766
+ removeAgentObjectIdField(JSON.parse(JSON.stringify(node))),
767
+ ),
768
+ };
769
+ } else {
770
+ throw new Error("Invalid source encountered when trying to create diff for remove edit");
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Creates a diff for a Move TreeEdit.
776
+ *
777
+ * @remarks
778
+ * This function must only be called BEFORE a move edit is applied.
779
+ * For move operations, the diff is created before the node(s) have been successfully moved,
780
+ * since the original index is needed to restore the node(s) if the move operation need to undo.
781
+ */
782
+ function createMoveDiff(
783
+ treeEdit: Move,
784
+ idGenerator: IdGenerator,
785
+ ): MoveSingleDiff | MoveRangeDiff {
786
+ const source = treeEdit.source;
787
+ const destination = treeEdit.destination;
788
+ const { array: destinationArrayNode } = getPlaceInfo(destination, idGenerator);
789
+
790
+ if (isObjectTarget(source)) {
791
+ const node = getNodeFromTarget(source, idGenerator);
792
+ return {
793
+ type: "move",
794
+ moveType: "move-single",
795
+ sourceNodePath: createNodePathRecursive(node, idGenerator, []),
796
+ destinationNodePath: createNodePathRecursive(destinationArrayNode, idGenerator, []),
797
+ aiExplanation: treeEdit.explanation,
798
+ nodeContent: removeAgentObjectIdField(JSON.parse(JSON.stringify(node))),
799
+ };
800
+ } else if (isRange(source)) {
801
+ const {
802
+ array,
803
+ startIndex: sourceStartIndex,
804
+ endIndex: sourceEndIndex,
805
+ } = getRangeInfo(source, idGenerator);
806
+
807
+ const movedNodePaths: NodePath[] = [];
808
+ const movedNodes: TreeNode[] = [];
809
+ for (let i = sourceStartIndex; i < sourceEndIndex; i++) {
810
+ const nodeToMove = array.at(i);
811
+ if (!isPrimitive(nodeToMove)) {
812
+ movedNodePaths.push(createNodePathRecursive(nodeToMove as TreeNode, idGenerator, []));
813
+ movedNodes.push(nodeToMove as TreeNode);
814
+ }
815
+ }
816
+
817
+ return {
818
+ type: "move",
819
+ moveType: "move-range",
820
+ sourceNodePaths: movedNodePaths,
821
+ destinationNodePath: createNodePathRecursive(destinationArrayNode, idGenerator, []),
822
+ aiExplanation: treeEdit.explanation,
823
+ nodeContents: movedNodes.map((node) =>
824
+ removeAgentObjectIdField(JSON.parse(JSON.stringify(node))),
825
+ ),
826
+ };
827
+ } else {
828
+ throw new Error("Invalid source for move edit");
829
+ }
830
+ }
831
+
555
832
  interface SchemaInfo {
556
833
  treeNodeSchema: TreeNodeSchema;
557
834
  simpleNodeSchema: new (dummy: unknown) => TreeNode;
@@ -25,6 +25,7 @@ import type {
25
25
  TokenLimits,
26
26
  TokenUsage,
27
27
  } from "../aiCollabApi.js";
28
+ import type { Diff } from "../diffTypes.js";
28
29
 
29
30
  import { applyAgentEdit } from "./agentEditReducer.js";
30
31
  import type { EditWrapper, TreeEdit } from "./agentEditTypes.js";
@@ -100,6 +101,7 @@ export interface GenerateTreeEditsOptions {
100
101
  interface GenerateTreeEditsSuccessResponse {
101
102
  status: "success";
102
103
  tokensUsed: TokenUsage;
104
+ readonly diffs: readonly Diff[];
103
105
  }
104
106
 
105
107
  interface GenerateTreeEditsErrorResponse {
@@ -111,6 +113,7 @@ interface GenerateTreeEditsErrorResponse {
111
113
  | "aborted"
112
114
  | "unexpectedError";
113
115
  tokensUsed: TokenUsage;
116
+ readonly diffs: readonly Diff[];
114
117
  }
115
118
 
116
119
  /**
@@ -128,6 +131,8 @@ export async function generateTreeEdits(
128
131
  ): Promise<GenerateTreeEditsSuccessResponse | GenerateTreeEditsErrorResponse> {
129
132
  const idGenerator = new IdGenerator();
130
133
  const editLog: EditLog = [];
134
+ const diffs: Diff[] = [];
135
+
131
136
  let editCount = 0;
132
137
  let sequentialErrorCount = 0;
133
138
 
@@ -165,8 +170,8 @@ export async function generateTreeEdits(
165
170
  simpleSchema.definitions,
166
171
  options.validator,
167
172
  );
168
- const explanation = result.explanation;
169
- editLog.push({ edit: { ...result, explanation } });
173
+ editLog.push({ edit: { ...result.edit } });
174
+ diffs.push(result.diff);
170
175
  sequentialErrorCount = 0;
171
176
 
172
177
  options.debugEventLogHandler?.({
@@ -201,6 +206,7 @@ export async function generateTreeEdits(
201
206
  editCount > 0 && sequentialErrorCount < editCount ? "partial-failure" : "failure",
202
207
  errorMessage: "unexpectedError",
203
208
  tokensUsed,
209
+ diffs,
204
210
  };
205
211
 
206
212
  if (options.limiters?.abortController?.signal.aborted === true) {
@@ -250,6 +256,7 @@ export async function generateTreeEdits(
250
256
  editCount > 0 && sequentialErrorCount < editCount ? "partial-failure" : "failure",
251
257
  errorMessage: "tokenLimitExceeded",
252
258
  tokensUsed,
259
+ diffs,
253
260
  };
254
261
  }
255
262
  throw error;
@@ -266,6 +273,7 @@ export async function generateTreeEdits(
266
273
  return {
267
274
  status: "success",
268
275
  tokensUsed,
276
+ diffs,
269
277
  };
270
278
  }
271
279
 
package/src/index.ts CHANGED
@@ -56,4 +56,19 @@ export {
56
56
  type EventFlowDebugEvent,
57
57
  } from "./aiCollabApi.js";
58
58
 
59
+ export {
60
+ type DiffBase,
61
+ type Diff,
62
+ type NodePath,
63
+ type InsertDiff,
64
+ type ModifyDiff,
65
+ type RemoveDiff,
66
+ type RemoveNodeDiff,
67
+ type ArraySingleRemoveDiff,
68
+ type ArrayRangeRemoveDiff,
69
+ type MoveDiff,
70
+ type MoveSingleDiff,
71
+ type MoveRangeDiff,
72
+ } from "./diffTypes.js";
73
+
59
74
  export { aiCollab } from "./aiCollab.js";