Docket.Graph (docket v0.1.1)

Copy Markdown View Source

Canonical, editable graph document used by host applications.

A Docket.Graph is the public build-time representation. Editors and compilers update this value directly; runtime lowering happens later through Docket.Graph.Compiler.

Non-bang editing functions return {:ok, graph} or {:error, %Docket.Graph.Error{}}, which is useful for event-by-event graph editors. Bang editing functions return the graph or raise, which keeps pipe-oriented graph construction ergonomic.

Functions that accept record attributes generally accept a keyword list, a map, or the matching Docket.Graph.* struct. Keyword lists are convenient for hand-written Elixir construction, maps are convenient for application/UI payloads, and structs let compilers or importers pass already-normalized graph records back through the same editing API.

Summary

Functions

Deletes an edge, raising on malformed arguments.

Deletes a field from both input and state field collections.

Deletes a field from both input and state field collections, raising on malformed arguments.

Deletes a node, raising on malformed arguments.

Reads diagnostics stored on the graph document.

Decodes a JSON-safe map into an authored graph, returning a tagged result.

Decodes a JSON-safe map into an authored graph, raising on invalid input.

Stores graph-level application metadata.

Stores graph-level application metadata, raising on malformed arguments.

Creates a new editable graph document.

Creates a new editable graph document, raising on invalid options.

Stores a graph-level policy value.

Stores a graph-level policy value, raising on malformed arguments.

Adds or replaces an edge by ID.

Adds or replaces an edge by ID, raising on malformed arguments.

Adds or replaces a state field.

Adds or replaces a state field, raising on malformed arguments.

Adds or replaces an input field.

Adds or replaces an input field, raising on malformed arguments.

Adds or replaces a node by ID.

Adds or replaces a node by ID, raising on malformed arguments.

Adds or replaces an output projection.

Adds or replaces an output projection, raising on malformed arguments.

Encodes the authored graph as a plain, JSON-safe map.

Updates an edge with a map/keyword patch or a function.

Updates an edge with a map/keyword patch or a function, raising on malformed arguments.

Updates a field with a map/keyword patch or a function.

Updates a field with a map/keyword patch or a function, raising on malformed arguments.

Updates a node with a map/keyword patch or a function.

Updates a node with a map/keyword patch or a function, raising on malformed arguments.

Verifies the graph and returns it with compiler diagnostics attached.

Types

edit_result()

@type edit_result() :: {:ok, t()} | {:error, Docket.Graph.Error.t()}

id()

@type id() :: String.t()

t()

@type t() :: %Docket.Graph{
  description: String.t() | nil,
  diagnostics: [Docket.Graph.Diagnostic.t()],
  edges: %{optional(id()) => Docket.Graph.Edge.t()},
  fields: %{optional(id()) => Docket.Graph.Field.t()},
  id: id(),
  inputs: %{optional(id()) => Docket.Graph.Field.t()},
  metadata: map(),
  name: String.t() | nil,
  nodes: %{optional(id()) => Docket.Graph.Node.t()},
  outputs: %{optional(id()) => Docket.Graph.Output.t()},
  policies: map(),
  schema_version: pos_integer()
}

Functions

delete_edge(graph, id, opts \\ [])

@spec delete_edge(t(), id(), keyword()) :: edit_result()

Deletes an edge.

delete_edge!(graph, id, opts \\ [])

@spec delete_edge!(t(), id(), keyword()) :: t()

Deletes an edge, raising on malformed arguments.

delete_field(graph, id, opts \\ [])

@spec delete_field(t(), id(), keyword()) :: edit_result()

Deletes a field from both input and state field collections.

delete_field!(graph, id, opts \\ [])

@spec delete_field!(t(), id(), keyword()) :: t()

Deletes a field from both input and state field collections, raising on malformed arguments.

delete_node(graph, id, opts \\ [])

@spec delete_node(t(), id(), keyword()) :: edit_result()

Deletes a node.

delete_node!(graph, id, opts \\ [])

@spec delete_node!(t(), id(), keyword()) :: t()

Deletes a node, raising on malformed arguments.

diagnostics(graph, opts \\ [])

@spec diagnostics(
  t(),
  keyword()
) :: [Docket.Graph.Diagnostic.t()]

Reads diagnostics stored on the graph document.

from_map(document, opts \\ [])

@spec from_map(
  map(),
  keyword()
) :: edit_result()

Decodes a JSON-safe map into an authored graph, returning a tagged result.

Returns {:ok, graph} or {:error, %Docket.Graph.Error{}}. The document is validated strictly: unknown structural keys, unknown enum values, malformed tagged expressions, an unsupported schema_version, and non-portable values are all rejected. Loading never creates atoms from the document - module implementation identifiers are looked up in the :implementations registry, and an identifier absent from the registry is a typed error.

See to_map/2 for the registry format and a full round-trip example.

Options

  • :implementations - registry of identifiers to module implementations, defaulting to %{}.

from_map!(document, opts \\ [])

@spec from_map!(
  map(),
  keyword()
) :: t()

Decodes a JSON-safe map into an authored graph, raising on invalid input.

Behaves like from_map/2 but returns the graph directly and raises Docket.Graph.Error on a malformed document or unknown implementation identifier.

metadata(graph, key, value, opts \\ [])

@spec metadata(t(), binary() | atom(), term(), keyword()) :: edit_result()

Stores graph-level application metadata.

metadata!(graph, key, value, opts \\ [])

@spec metadata!(t(), binary() | atom(), term(), keyword()) :: t()

Stores graph-level application metadata, raising on malformed arguments.

Content is stored as given. Public interchange and publication normalize open atom keys and values to strings through the same runtime value boundary; terms without a portable representation are rejected. Keys starting with "$" are reserved.

new(opts \\ [])

@spec new(keyword()) :: edit_result()

Creates a new editable graph document.

Options must be a keyword list. Supported options are:

  • :id - graph ID; generated when omitted (via :id_generator when given)
  • :name - optional display name
  • :description - optional description
  • :schema_version - Docket graph document schema version
  • :metadata - application metadata map
  • :policies - graph policy map

new!(opts \\ [])

@spec new!(keyword()) :: t()

Creates a new editable graph document, raising on invalid options.

policy(graph, key, value, opts \\ [])

@spec policy(t(), binary() | atom(), term(), keyword()) :: edit_result()

Stores a graph-level policy value.

policy!(graph, key, value, opts \\ [])

@spec policy!(t(), binary() | atom(), term(), keyword()) :: t()

Stores a graph-level policy value, raising on malformed arguments.

Content is stored as given. Public interchange and publication normalize open atom keys and values to strings through the same runtime value boundary; terms without a portable representation are rejected. Keys starting with "$" are reserved.

put_edge(graph, id, attrs, opts \\ [])

@spec put_edge(t(), id(), keyword() | map() | Docket.Graph.Edge.t(), keyword()) ::
  edit_result()

Adds or replaces an edge by ID.

attrs may be a keyword list, a map, or a Docket.Graph.Edge struct. The explicit id argument is used as the stored edge ID.

put_edge!(graph, id, attrs, opts \\ [])

@spec put_edge!(t(), id(), keyword() | map() | Docket.Graph.Edge.t(), keyword()) ::
  t()

Adds or replaces an edge by ID, raising on malformed arguments.

put_field(graph, id, attrs, opts \\ [])

@spec put_field(t(), id(), keyword() | map() | Docket.Graph.Field.t(), keyword()) ::
  edit_result()

Adds or replaces a state field.

attrs may be a keyword list, a map, or a Docket.Graph.Field struct. The explicit id argument is used as the stored field ID, and the stored field kind is forced to :state. :schema accepts a Docket.Schema or schema shorthand (see Docket.Schema.normalize/1).

put_field!(graph, id, attrs, opts \\ [])

@spec put_field!(t(), id(), keyword() | map() | Docket.Graph.Field.t(), keyword()) ::
  t()

Adds or replaces a state field, raising on malformed arguments.

put_input(graph, id, attrs, opts \\ [])

@spec put_input(t(), id(), keyword() | map() | Docket.Graph.Field.t(), keyword()) ::
  edit_result()

Adds or replaces an input field.

attrs may be a keyword list, a map, or a Docket.Graph.Field struct. The explicit id argument is used as the stored field ID, and the stored field kind is forced to :input. :schema accepts a Docket.Schema or schema shorthand (see Docket.Schema.normalize/1).

put_input!(graph, id, attrs, opts \\ [])

@spec put_input!(t(), id(), keyword() | map() | Docket.Graph.Field.t(), keyword()) ::
  t()

Adds or replaces an input field, raising on malformed arguments.

put_node(graph, id, attrs, opts \\ [])

@spec put_node(t(), id(), keyword() | map() | Docket.Graph.Node.t(), keyword()) ::
  edit_result()

Adds or replaces a node by ID.

attrs may be a keyword list, a map, or a Docket.Graph.Node struct. The explicit id argument is used as the stored node ID.

:implementation accepts shorthand module forms:

  • MyNode becomes %{type: :module, module: MyNode, function: :call}
  • {MyNode, :run} becomes %{type: :module, module: MyNode, function: :run}
  • maps are preserved for compiler/runtime validation

:inputs and :fields declare graph fields inline: each entry maps a field ID to either field attrs (everything put_input!/put_field! accepts) or bare schema shorthand, and materializes as an ordinary graph field — nothing lands on the node record:

Graph.put_node!(graph, "draft",
  implementation: MyApp.Nodes.LLM,
  inputs: %{"customer_message" => [schema: :string, required: true]},
  fields: %{
    "draft_response" => :string,
    "messages" => [schema: {:list, :map}, reducer: :append]
  }
)

Declaring a field that already exists with an identical definition is a no-op, so multiple nodes can declare a shared field in any order. A conflicting definition raises ({:error, %Docket.Graph.Error{}} from put_node/4); redefinition stays explicit via put_input!/put_field!. Deleting the node later does not delete the fields — they are shared state, and verify/2 flags fields nothing references.

put_node!(graph, id, attrs, opts \\ [])

@spec put_node!(t(), id(), keyword() | map() | Docket.Graph.Node.t(), keyword()) ::
  t()

Adds or replaces a node by ID, raising on malformed arguments.

put_output(graph, id, attrs, opts \\ [])

@spec put_output(t(), id(), keyword() | map() | Docket.Graph.Output.t(), keyword()) ::
  edit_result()

Adds or replaces an output projection.

attrs may be a keyword list, a map, or a Docket.Graph.Output struct. The explicit id argument is used as the stored output ID. If :source is omitted, it defaults to the output ID. :schema accepts a Docket.Schema or schema shorthand (see Docket.Schema.normalize/1).

put_output!(graph, id, attrs, opts \\ [])

@spec put_output!(t(), id(), keyword() | map() | Docket.Graph.Output.t(), keyword()) ::
  t()

Adds or replaces an output projection, raising on malformed arguments.

to_map(graph, opts \\ [])

@spec to_map(
  t(),
  keyword()
) :: map()

Encodes the authored graph as a plain, JSON-safe map.

The returned map uses string keys everywhere and only JSON-safe values (binaries, numbers, booleans, nil, lists, and string-keyed maps), so a host can persist or transmit it with any JSON codec. Atom keys and values in open content (metadata, policies, config, schema defaults, reducer opts) are coerced to strings; terms with no JSON representation (tuples, keyword lists, PIDs, references, functions, MapSets, structs) raise Docket.Graph.Error. Keys starting with "$" are reserved for the wire format and are rejected in open content. Transient diagnostics are omitted, and "schema_version" is always present.

Executable node implementations are resolved through the :implementations registry: a map of stable, host-defined string identifiers to module implementations. Each value may be a module (MyNode), a {module, function} tuple, or a %{type: :module, module: MyNode, function: :call} map. The registry must be unambiguous - two identifiers may not resolve to the same {module, function} pair. A module implementation that is absent from the registry raises. Passthrough (non-module) implementation maps round-trip as plain durable values.

Options

  • :implementations - registry of identifiers to module implementations, defaulting to %{}. Required only when a graph actually uses module implementations.

Example

registry = %{
  "watercooler.agent" => WaterCooler.Workflows.Nodes.SpawnAgent,
  "watercooler.tap" => {WaterCooler.Workflows.Nodes.Tap, :call}
}

json =
  graph
  |> Docket.Graph.to_map(implementations: registry)
  |> Jason.encode!()

{:ok, graph} =
  json
  |> Jason.decode!()
  |> Docket.Graph.from_map(implementations: registry)

This is the editable AUTHORED graph document. It neither contains nor recomputes a Docket.GraphRef hash: Docket.save_graph/3 materializes node defaults and privately encodes and hashes the effective graph to produce a content-addressed reference. Re-saving an authored graph after a node's defaults change may therefore produce a different effective graph reference even when this document is unchanged.

update_edge(graph, id, attrs_or_fun, opts \\ [])

@spec update_edge(
  t(),
  id(),
  (Docket.Graph.Edge.t() -> Docket.Graph.Edge.t() | map() | keyword())
  | Docket.Graph.Edge.t()
  | map()
  | keyword(),
  keyword()
) :: edit_result()

Updates an edge with a map/keyword patch or a function.

attrs_or_fun may be:

The explicit id argument remains the stored edge ID after the update.

update_edge!(graph, id, attrs_or_fun, opts \\ [])

@spec update_edge!(
  t(),
  id(),
  (Docket.Graph.Edge.t() -> Docket.Graph.Edge.t() | map() | keyword())
  | Docket.Graph.Edge.t()
  | map()
  | keyword(),
  keyword()
) :: t()

Updates an edge with a map/keyword patch or a function, raising on malformed arguments.

update_field(graph, id, attrs_or_fun, opts \\ [])

@spec update_field(
  t(),
  id(),
  (Docket.Graph.Field.t() -> Docket.Graph.Field.t() | map() | keyword())
  | Docket.Graph.Field.t()
  | map()
  | keyword(),
  keyword()
) :: edit_result()

Updates a field with a map/keyword patch or a function.

attrs_or_fun may be:

The resulting field kind decides whether the field is stored as an input or a state field. The explicit id argument remains the stored field ID.

update_field!(graph, id, attrs_or_fun, opts \\ [])

@spec update_field!(
  t(),
  id(),
  (Docket.Graph.Field.t() -> Docket.Graph.Field.t() | map() | keyword())
  | Docket.Graph.Field.t()
  | map()
  | keyword(),
  keyword()
) :: t()

Updates a field with a map/keyword patch or a function, raising on malformed arguments.

update_node(graph, id, attrs_or_fun, opts \\ [])

@spec update_node(
  t(),
  id(),
  (Docket.Graph.Node.t() -> Docket.Graph.Node.t() | map() | keyword())
  | Docket.Graph.Node.t()
  | map()
  | keyword(),
  keyword()
) :: edit_result()

Updates a node with a map/keyword patch or a function.

attrs_or_fun may be:

The explicit id argument remains the stored node ID after the update.

update_node!(graph, id, attrs_or_fun, opts \\ [])

@spec update_node!(
  t(),
  id(),
  (Docket.Graph.Node.t() -> Docket.Graph.Node.t() | map() | keyword())
  | Docket.Graph.Node.t()
  | map()
  | keyword(),
  keyword()
) :: t()

Updates a node with a map/keyword patch or a function, raising on malformed arguments.

verify(graph, opts \\ [])

@spec verify(
  t(),
  keyword()
) :: {:ok, t()} | {:error, t()}

Verifies the graph and returns it with compiler diagnostics attached.