From 35774fbbdbd68dadb30ebc98278fae60141d21e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:59:33 +0700 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20resolve=20#1166=20=E2=80=94=20when?= =?UTF-8?q?=20a=20node=20receives=20specific=20properties=20from=20one=20n?= =?UTF-8?q?ode=20and=20`*`=20properties=20from=20another=20node=20a=20race?= =?UTF-8?q?=20condition=20is=20caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1166 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- packages/breadboard/src/node.ts | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 packages/breadboard/src/node.ts diff --git a/packages/breadboard/src/node.ts b/packages/breadboard/src/node.ts new file mode 100644 index 00000000000..21ed9098c3f --- /dev/null +++ b/packages/breadboard/src/node.ts @@ -0,0 +1,35 @@ +export interface WireOptions { + required?: boolean; +} + +export class Wire { + source: Node; + target: Node; + required: boolean; + + constructor(source: Node, target: Node, required: boolean = false) { + this.source = source; + this.target = target; + this.required = required; + } +} + +export class Node { + id: string; + outgoing: Wire[] = []; + + constructor(id: string) { + this.id = id; + this.outgoing = []; + } + + to(target: Node, options?: WireOptions): Wire { + const wire = new Wire(this, target, options?.required); + this.outgoing.push(wire); + return wire; + } + + requiredTo(target: Node): Wire { + return this.to(target, { required: true }); + } +} \ No newline at end of file From 24cef9287794a617a6c8e3c7809a389477d00965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:59:34 +0700 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20resolve=20#1166=20=E2=80=94=20when?= =?UTF-8?q?=20a=20node=20receives=20specific=20properties=20from=20one=20n?= =?UTF-8?q?ode=20and=20`*`=20properties=20from=20another=20node=20a=20race?= =?UTF-8?q?=20condition=20is=20caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1166 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- packages/breadboard/src/runtime/traversal.ts | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/breadboard/src/runtime/traversal.ts diff --git a/packages/breadboard/src/runtime/traversal.ts b/packages/breadboard/src/runtime/traversal.ts new file mode 100644 index 00000000000..8f78b269154 --- /dev/null +++ b/packages/breadboard/src/runtime/traversal.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Edge, NodeIdentifier, InputValues } from "../types.js"; + +interface TraversalEdge { + from: NodeIdentifier; + to: NodeIdentifier; + out: string; + in: string; + required?: boolean; +} + +interface NodeState { + inputs: InputValues; + receivedEdges: Set; +} + +export class Traversal { + private incomingEdges: Map = new Map(); + private nodeStates: Map = new Map(); + + constructor(edges: Edge[]) { + for (const edge of edges) { + const edgesForNode = this.incomingEdges.get(edge.to) || []; + edgesForNode.push(edge as TraversalEdge); + this.incomingEdges.set(edge.to, edgesForNode); + } + } + + /** + * Check if a node is ready to execute. + * A node is ready only when all required incoming edges have provided data. + */ + isNodeReady(nodeId: NodeIdentifier): boolean { + const edges = this.incomingEdges.get(nodeId) || []; + const state = this.nodeStates.get(nodeId); + + for (const edge of edges) { + if (edge.required) { + const edgeKey = `${edge.from}:${edge.out}`; + if (!state?.receivedEdges.has(edgeKey)) { + return false; + } + } + } + + return true; + } + + /** + * Receive data from an edge. + */ + receiveEdgeData(nodeId: NodeIdentifier, from: NodeIdentifier, out: string, data: unknown): void { + let state = this.nodeStates.get(nodeId); + if (!state) { + state = { inputs: {}, receivedEdges: new Set() }; + this.nodeStates.set(nodeId, state); + } + + state.receivedEdges.add(`${from}:${out}`); + + if (data !== undefined && typeof data === "object") { + Object.assign(state.inputs, data); + } + } + + getNodeInputs(nodeId: NodeIdentifier): InputValues { + return this.nodeStates.get(nodeId)?.inputs || {}; + } +} \ No newline at end of file From af4d28984b3384eed85096dab5c03861b4ab937c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:59:35 +0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20resolve=20#1166=20=E2=80=94=20when?= =?UTF-8?q?=20a=20node=20receives=20specific=20properties=20from=20one=20n?= =?UTF-8?q?ode=20and=20`*`=20properties=20from=20another=20node=20a=20race?= =?UTF-8?q?=20condition=20is=20caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1166 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- packages/breadboard/src/types.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/breadboard/src/types.ts diff --git a/packages/breadboard/src/types.ts b/packages/breadboard/src/types.ts new file mode 100644 index 00000000000..39d7528f80a --- /dev/null +++ b/packages/breadboard/src/types.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export type NodeIdentifier = string; + +/** + * Represents an edge (wire) connecting two nodes in the graph. + */ +export interface Edge { + from: NodeIdentifier; + to: NodeIdentifier; + out?: string; + in?: string; + required?: boolean; +} + +/** + * Proxy interface for building node connections in a fluent API style. + */ +export interface NodeProxy { + to(target: NodeIdentifier): void; + requiredTo(target: NodeIdentifier): void; +} \ No newline at end of file From 75b785cece059ee97c88954885ce0ebd72c3d0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:59:36 +0700 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20resolve=20#1166=20=E2=80=94=20when?= =?UTF-8?q?=20a=20node=20receives=20specific=20properties=20from=20one=20n?= =?UTF-8?q?ode=20and=20`*`=20properties=20from=20another=20node=20a=20race?= =?UTF-8?q?=20condition=20is=20caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1166 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- packages/breadboard/src/board.ts | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/breadboard/src/board.ts diff --git a/packages/breadboard/src/board.ts b/packages/breadboard/src/board.ts new file mode 100644 index 00000000000..ca7056f77ec --- /dev/null +++ b/packages/breadboard/src/board.ts @@ -0,0 +1,40 @@ +import type { GraphDescriptor, Edge, NodeDescriptor } from "./types.js"; + +export class Board { + private edges: Edge[] = []; + private nodes: NodeDescriptor[] = []; + + serialize(): GraphDescriptor { + return { + edges: this.edges.map((edge) => { + const serialized: Edge = { + from: edge.from, + to: edge.to, + }; + if (edge.out !== undefined) serialized.out = edge.out; + if (edge.in !== undefined) serialized.in = edge.in; + if (edge.required !== undefined) serialized.required = edge.required; + return serialized; + }), + nodes: this.nodes, + }; + } + + static deserialize(descriptor: GraphDescriptor): Board { + const board = new Board(); + board.nodes = descriptor.nodes || []; + + for (const edge of descriptor.edges || []) { + const newEdge: Edge = { + from: edge.from, + to: edge.to, + }; + if (edge.out !== undefined) newEdge.out = edge.out; + if (edge.in !== undefined) newEdge.in = edge.in; + if (edge.required !== undefined) newEdge.required = edge.required; + board.edges.push(newEdge); + } + + return board; + } +} \ No newline at end of file