Please note:this document is a work in progress.

Editor API

The Editor API provides a way to edit a graph. It is designed to work in conjunction with the Inspector API and helps ensure that the graph edits retain their structural integrity.

Note

The full list of types of Editor API can be found in /packages/breadboard/src/editor/types.ts

Creating an editor

Calling the edit method creates an instance of an EditableGraph. This method expects a GraphDescriptor as its first argument:

import { edit } from "google-labs/breadboard";

// Returns an instance of `EditableGraph`.
const graph = edit(bgl);

The editor API provides one method for applying edits to the graph: edit. This method takes three members (two required):

// Adds a node with id = "foo" and type = "type".
// Returns `Promise<EditResult>`.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" }, graphId: "" }],
  `Create Node "foo"`
);
if (!result.success) {
  console.warn("Adding node failed with this error", result.error);
}

The string label plays an important role. It groups the edit operations for the purpose of collecting history. See more about how to use it in the Graph history management (undo/redo) section.

When dryRun is set to true, the method will not perform the actual edit, but report the result as if the edit as applied. This is useful if we want to check whether an edit would be valid without actually making an edit.

// Does not actually add node with id = "foo" and type = "type",
// just checks to see if such a node could be added.
// Returns `Promise<EditResult>`.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" }, graphId: "" }],
  "Adding Node (dry run)",
  true
);
if (!result.success) {
  console.warn("Adding node will fail with this error", result.error);
} else {
  console.log("Yay, we can add this node, proceed forth");
}

Multiple changes to the graph are performed as one atomic unit when specified in the same method:

// Adds a node with id = "foo" and type = "type" ...
// .. and a node with id = "bar" and type = "type" as one atomic operation.
// Returns `Promise<EditResult>`.
const result = await graph.edit([
  { type: "addnode", node: { id: "foo", type: "type" }, graphId: "" },
  { type: "addnode", node: { id: "bar", type: "type" }, graphId: "" },
]);
if (!result.success) {
  console.warn("Adding node failed with this error", result.error);
}

Kits

To ensure that edit method does not jeopardize the integrity of the graph, we need to supply the editor a list of kits. Kits are collections of functions that are invoked during running the graph. We can provide kits as kits property on the second, optional EditableGraphOptions argument of the edit method:

import Core from "@google-labs/core-kit";
import JSONKit from "@google-labs/json-kit";

const graph = edit(bgl, { kits: [asRuntime(Core), asRunTime(JSONKit)] });

Editing a graph

Here are all the edit operations that we can perform on the graph:

Starting a new graph

If we want to start a brand-new graph, we can use the handy blank method, provided by the Editor API:

import { blank } from "google-labs/breadboard";

// Returns a new `GraphDescriptor` instance
// of a pre-built blank graph.
const myNewGraph = blank();

The newly-created graph will have a pre-filled title and description, a version of 0.0.1 and two connected nodes: the input node connected to the output node with one wire. The wire will go from text port to text port of the respective nodes.

Blank graph diagram

Graph versioning

To help us keep track of the edits, the EditableGraph has a version() method, which returns the current version of the graph:

// Returns a number.
const current = graph.version();

By default, a new EditableGraph instance starts with version 0 and increments it for each change.

To supply a different starting version, use the version option when creating a new EditableGraph instance:

import Core from "@google-labs/core-kit";
import JSONKit from "@google-labs/json-kit";

// Let's start with version 1000.
const version = 1000;
const graph = edit(bgl, {
  kits: [asRuntime(Core), asRunTime(JSONKit)],
  version,
});

Graph history management (undo/redo)

In addition to simple versioning, the Editor API tracks history of the graph to enable undo/redo capability. The history() method on an EditableGraph instance provides a few handy helpers for that:

// Returns an `EditHistory` instance.
const history = graph.history();
if (history.canUndo()) {
  history.undo();
}
history.redo();

// Prints out a list of history entries with a ">" marker next
// to the current history entry.
const labels = history.entries().map((entry) => entry.label);
console.group("History:");
labels.forEach((label, index) => {
  const current = index === history.index() ? ">" : " ";
  console.log(`${index}:${current} ${label}`);
});
console.groupEnd();

The string label that was supplied for an edit operation allows the user of the API to group multiple edit operations into a single history entry.

Each edit call that has the same label as the previous edit call will be grouped with that previous call: no new history entry will be created for it.

When the edit call has a label that's different from the previous call, a new history entry will be created.

// Creates a new history entry.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "foo", type: "type" }, graphId: "" }],
  `Create Node "foo"`
);
// Different label, creates another history entry.
const result = await graph.edit(
  [
    {
      type: "changemetadata",
      id: "foo",
      metadata: { title: "F" },
      graphId: "",
    },
  ],
  `Editing metadata for node "foo"`
);
// Label is the same as the previous call, no new entry created.
const result = await graph.edit(
  [
    {
      type: "changemetadata",
      id: "foo",
      metadata: { title: "Fo" },
      graphId: "",
    },
  ],
  `Editing metadata for node "foo"`
);
// Label is the same as the previous call, no new entry created.
const result = await graph.edit(
  [
    {
      type: "changemetadata",
      id: "foo",
      metadata: { title: "Foo" },
      graphId: "",
    },
  ],
  `Editing metadata for node "foo"`
);
// Different label, creates another history entry.
const result = await graph.edit(
  [{ type: "addnode", node: { id: "bar", type: "type" }, graphId: "" }],
  `Create Node "bar"`
);

Editing subgraphs

Since every graph may have embedded subgraphs in it, we can use the Editor API to access and edit these subgraphs as well. Every subgraph has an identifier that is unique among all subgraphs within their graph. The API uses this id to add, remove, replace subgraphs.

The graphId property in the edit operations provides a way to identify the particular subgraph on which it operates.

The id of the main graph is an empty string: "".

Accessing the graph

To access the underlying GraphDescriptor instance, use the raw() method on the EditableGraph instance.

const graph = edit(bgl, { kits });
await graph.addNode({ id: "foo", type: "bar" });
// Returns the `GraphDescriptor` instance.
const newBgl = graph.raw();

The raw() method will correctly serialize all of graph's subgraph and reflect their edits.

Inspecting the graph

Because the graph constantly changes, it can be tedious to keep track of the latest changes and keep creating new instances of InspectableGraph. To help with that, there's an inspect method on the EditableGraph.

The inspect method takes a graph identifier, allow easy access to a particular sub-graph's InspectableGraph instance.

// Guaranteed to be inspecting the latest graph.
// Returns `InspectableGraph`.
const inspectableGraph = graph.inspect("");

Listening to changes

The EditableGraph instance also the addEventListener method, which works pretty much like any EventTarget -- we can subscribe to listen to events that are dispatched by this instance. Currently the following events are supported:

Transforms

Transforms are an abstraction that allows encapsulating a large atomic edit with many moving parts. For instance, if we want to create a new subgraph and move a few existing nodes from the main graph into it, we can structure it as a list of edits and check to make sure that each edit is valid. Or, we can use this transform:

const moving = await editor.apply(
  new MoveToNewGraphTransform(
    // Move nodes "node-1" and "node-2" along with all of their shared
    // edges...
    ["node-1", "node-2"],
    // .. From main graph ...
    "",
    // ... To a new subgraph with the id "foo" ...
    "foo",
    // .. the title "Title" ...
    "Title",
    // ... and a description "Description".
    "Description"
  )
);

Transforms are more flexible than the list of operations (even though they produce lists of operations as a result), because they allow us to run code between operations.

Currently, here are the built-in transforms available from @google-labs/breadboard package:

const sidewired = await editor.apply(
  new ConfigureSidewireTransform(
    "node0", // node id of the node to configure
    "$side", // sidewire port name for the specified node
    "", // graph id of the specified node ("" means main graph)
    "foo" // graph id of the graph to which we're drawing the subwire
  )
);
if (!sidewired.success) {
  // handle error
}
const isolating = await graph.apply(new IsolateSelectionTransform(
  ["node0"], // list of nodes ids to isolate
  "" // graph id
));
(!isolating.success) {
  // handle error
}
const merging = await editor.apply(
  new MergeGraphTransform(
    "foo", // source graph id
    "" // graph id to merge into
  )
);
if (!merging.success) {
  // handle error
}
const moving = await editor.apply(
  new MoveToGraphTransform(
    ["node10"], // list of node ids to move
    "foo", // source graph id
    "" // destination graph id
  )
);
if (!moving.success) {
  // handle error
}
const sidewired = await editor.apply(
  new SidewireToNewGraphTransform(
    "node0", //  The id of the node on which to configure the subwire.
    "$side", // The port name that will be used to configure the subwire.
    "", // The graph id of the specified node ("" means main board).
    "foo", // The id of the graph to create from selected nodes.
    ["node2"], // The nodes that will be moved to new graph.
    "Hello", //  The title of the newly created graph.
    "World" // The description of the newly created graph (default "")
  )
);