Skip to content

Commit 6ada218

Browse files
authored
[build] Serialize boards as subgraphs when passed as node inputs (#2185)
Input ports with the "board" behavior can now receive board objects declared using the build API. This will be serialized as an embedded graph.
1 parent e7bdeaa commit 6ada218

File tree

11 files changed

+345
-23
lines changed

11 files changed

+345
-23
lines changed

.changeset/silly-baboons-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@breadboard-ai/build": minor
3+
---
4+
5+
Input ports with the "board" behavior can now receive board objects declared using the build API. This will be serialized as an embedded graph.

packages/build/src/internal/board/board.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export function board<
7171
description,
7272
version,
7373
metadata,
74+
isBoard: true,
7475
});
7576
}
7677

@@ -264,3 +265,9 @@ type ExtractPortTypes<PORTS extends BoardInputPorts | BoardOutputPorts> = {
264265
? TYPE
265266
: never;
266267
};
268+
269+
export function isBoard(value: unknown): value is GenericBoardDefinition {
270+
return (
271+
typeof value === "function" && "isBoard" in value && value.isBoard === true
272+
);
273+
}

packages/build/src/internal/board/serialize.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import { ConstantVersionOf, isConstant } from "./constant.js";
2323
import { isConvergence } from "./converge.js";
2424
import type { GenericSpecialInput, Input, InputWithDefault } from "./input.js";
2525
import { isLoopback } from "./loopback.js";
26-
import { isOptional, OptionalVersionOf } from "./optional.js";
26+
import { OptionalVersionOf, isOptional } from "./optional.js";
2727
import type { Output } from "./output.js";
28+
import { isBoard, type GenericBoardDefinition } from "./board.js";
2829

2930
/**
3031
* Serialize a Breadboard board to Breadboard Graph Language (BGL) so that it
@@ -33,8 +34,10 @@ import type { Output } from "./output.js";
3334
export function serialize(board: SerializableBoard): GraphDescriptor {
3435
const nodes = new Map<object, NodeDescriptor>();
3536
const edges: Edge[] = [];
37+
const graphs = new Map<string, GraphDescriptor>();
3638
const errors: string[] = [];
3739
const typeCounts = new Map<string, number>();
40+
let nextEmbeddedGraphId = 0;
3841

3942
// Prepare our main input and output nodes. They represent the overall
4043
// signature of the board.
@@ -275,7 +278,7 @@ export function serialize(board: SerializableBoard): GraphDescriptor {
275278
);
276279
}
277280

278-
return {
281+
const bgl: GraphDescriptor = {
279282
...(board.title ? { title: board.title } : {}),
280283
...(board.description ? { description: board.description } : {}),
281284
...(board.version ? { version: board.version } : {}),
@@ -302,6 +305,10 @@ export function serialize(board: SerializableBoard): GraphDescriptor {
302305
...[...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
303306
],
304307
};
308+
if (graphs.size > 0) {
309+
bgl.graphs = Object.fromEntries([...graphs]);
310+
}
311+
return bgl;
305312

306313
function visitNodeAndReturnItsId(node: SerializableNode): string {
307314
let descriptor = nodes.get(node);
@@ -409,6 +416,14 @@ export function serialize(board: SerializableBoard): GraphDescriptor {
409416
throw new Error(
410417
`Internal error: value was a symbol (${String(value)}) for a ${inputPort.node.type}:${inputPort.name} port.`
411418
);
419+
} else if (isBoard(value)) {
420+
configurationEntries.push([
421+
portName,
422+
{
423+
kind: "board",
424+
path: `#${embedBoardAndReturnItsId(value)}`,
425+
},
426+
]);
412427
} else {
413428
configurationEntries.push([
414429
portName,
@@ -450,6 +465,13 @@ export function serialize(board: SerializableBoard): GraphDescriptor {
450465
typeCounts.set(type, count + 1);
451466
return `${type}-${count}`;
452467
}
468+
469+
function embedBoardAndReturnItsId(board: GenericBoardDefinition): string {
470+
const id = `subgraph-${nextEmbeddedGraphId}`;
471+
nextEmbeddedGraphId++;
472+
graphs.set(id, serialize(board));
473+
return id;
474+
}
453475
}
454476

455477
function isSpecialInput(value: unknown): value is GenericSpecialInput {

packages/build/src/internal/common/serializable.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type { GraphMetadata } from "@google-labs/breadboard-schema/graph.js";
8+
import type { GenericBoardDefinition } from "../board/board.js";
79
import type { Convergence } from "../board/converge.js";
810
import type {
911
GenericSpecialInput,
1012
Input,
1113
InputWithDefault,
1214
} from "../board/input.js";
13-
import type { Output } from "../board/output.js";
1415
import type { Loopback } from "../board/loopback.js";
16+
import type { Output } from "../board/output.js";
1517
import type { BreadboardType, JsonSerializable } from "../type-system/type.js";
1618
import type { DefaultValue, OutputPortGetter } from "./port.js";
17-
import type { GraphMetadata } from "@google-labs/breadboard-schema/graph.js";
1819

1920
export interface SerializableBoard {
2021
inputs: Record<
@@ -71,6 +72,7 @@ export interface SerializableInputPort {
7172
| GenericSpecialInput
7273
| Loopback<JsonSerializable>
7374
| Convergence<JsonSerializable>
75+
| GenericBoardDefinition
7476
| typeof DefaultValue;
7577
}
7678

packages/build/src/internal/define/define.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ export function defineNodeType<
156156
GetOptionalInputs<I> & keyof Expand<GetStaticTypes<I>>,
157157
GetReflective<O>,
158158
Expand<GetPrimary<I>>,
159-
Expand<GetPrimary<O>>
159+
Expand<GetPrimary<O>>,
160+
Expand<ExtractInputMetadata<I>>
160161
> {
161162
if (!params.name) {
162163
throw new Error("params.name is required");
@@ -178,7 +179,8 @@ export function defineNodeType<
178179
GetOptionalInputs<I> & keyof Expand<GetStaticTypes<I>>,
179180
GetReflective<O>,
180181
Expand<GetPrimary<I>>,
181-
Expand<GetPrimary<O>>
182+
Expand<GetPrimary<O>>,
183+
Expand<ExtractInputMetadata<I>>
182184
>(
183185
params.name,
184186
omitDynamic(params.inputs),
@@ -194,9 +196,30 @@ export function defineNodeType<
194196
invoke: impl.invoke.bind(impl),
195197
describe: impl.describe.bind(impl),
196198
metadata: params.metadata || {},
197-
});
199+
// TODO(aomarks) Should not need cast.
200+
}) as Definition<
201+
Expand<GetStaticTypes<I>>,
202+
Expand<GetStaticTypes<O>>,
203+
GetDynamicTypes<I>,
204+
GetDynamicTypes<O>,
205+
GetOptionalInputs<I> & keyof Expand<GetStaticTypes<I>>,
206+
GetReflective<O>,
207+
Expand<GetPrimary<I>>,
208+
Expand<GetPrimary<O>>,
209+
Expand<ExtractInputMetadata<I>>
210+
>;
198211
}
199212

213+
type ExtractInputMetadata<I extends Record<string, InputPortConfig>> = {
214+
[K in keyof I as K extends "*" ? never : K]: {
215+
board: I[K]["behavior"] extends Array<unknown>
216+
? "board" extends I[K]["behavior"][number]
217+
? true
218+
: false
219+
: false;
220+
};
221+
};
222+
200223
function omitDynamic(configs: PortConfigs): PortConfigs {
201224
return Object.fromEntries(
202225
Object.entries(configs).filter(([name]) => name !== "*")

packages/build/src/internal/define/definition.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { array } from "../type-system/array.js";
5151
import { object } from "../type-system/object.js";
5252
import { normalizeBreadboardError } from "../common/error.js";
5353
import type { Convergence } from "../board/converge.js";
54+
import type { BoardDefinition } from "../board/board.js";
5455

5556
export interface Definition<
5657
/* Static Inputs */ SI extends { [K: string]: JsonSerializable },
@@ -61,9 +62,10 @@ export interface Definition<
6162
/* Reflective? */ R extends boolean,
6263
/* Primary Input */ PI extends string | false,
6364
/* Primary Output */ PO extends string | false,
65+
/* Input Metadata */ IM extends { [K: string]: InputMetadata },
6466
> extends StrictNodeHandler {
6567
<A extends LooseInstantiateArgs>(
66-
args: A & StrictInstantiateArgs<SI, OI, DI, A>
68+
args: A & StrictInstantiateArgs<SI, OI, DI, A, IM>
6769
): Instance<
6870
InstanceInputs<SI, DI, A>,
6971
InstanceOutputs<SI, SO, DO, R, A>,
@@ -83,6 +85,7 @@ export class DefinitionImpl<
8385
/* Reflective? */ R extends boolean,
8486
/* Primary Input */ PI extends string | false,
8587
/* Primary Output */ PO extends string | false,
88+
/* Input Metadata */ IM extends { [K: string]: InputMetadata },
8689
> implements StrictNodeHandler
8790
{
8891
readonly #name: string;
@@ -135,7 +138,7 @@ export class DefinitionImpl<
135138
}
136139

137140
instantiate<A extends LooseInstantiateArgs>(
138-
args: A & StrictInstantiateArgs<SI, OI, DI, A>
141+
args: A & StrictInstantiateArgs<SI, OI, DI, A, IM>
139142
): Instance<
140143
InstanceInputs<SI, DI, A>,
141144
InstanceOutputs<SI, SO, DO, R, A>,
@@ -396,6 +399,14 @@ export class DefinitionImpl<
396399
}
397400
}
398401

402+
/**
403+
* Extra data about inputs. This is here to stop growing the number of
404+
* parameters on Definition.
405+
*/
406+
export type InputMetadata = {
407+
board: boolean;
408+
};
409+
399410
function parseDynamicPorts(
400411
ports: Exclude<CustomDescribePortManifest, UnsafeSchema>,
401412
base: DynamicInputPortConfig | DynamicOutputPortConfig
@@ -426,14 +437,20 @@ type StrictInstantiateArgs<
426437
OI extends keyof SI,
427438
DI extends JsonSerializable | undefined,
428439
A extends LooseInstantiateArgs,
440+
IM extends { [K: string]: InputMetadata },
429441
> = {
430442
$id?: string;
431443
$metadata?: {
432444
title?: string;
433445
description?: string;
434446
};
435447
} & {
436-
[K in keyof Omit<SI, OI | "$id" | "$metadata">]: InstantiateArg<SI[K]>;
448+
[K in keyof Omit<SI, OI | "$id" | "$metadata">]: IM[K extends keyof IM
449+
? K
450+
: never]["board"] extends true
451+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
452+
InstantiateArg<SI[K]> | BoardDefinition<any, any>
453+
: InstantiateArg<SI[K]>;
437454
} & {
438455
[K in OI]?:
439456
| InstantiateArg<SI[K]>

packages/build/src/internal/define/node-factory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { Expand } from "../common/type-util.js";
1818
* for use with {@link KitBuilder}.
1919
*/
2020
export type NodeFactoryFromDefinition<
21-
D extends Definition<any, any, any, any, any, any, any, any>,
21+
D extends Definition<any, any, any, any, any, any, any, any, any>,
2222
> =
2323
D extends Definition<
2424
infer SI,
@@ -28,6 +28,7 @@ export type NodeFactoryFromDefinition<
2828
infer OI,
2929
any,
3030
any,
31+
any,
3132
any
3233
>
3334
? NewNodeFactory<

packages/build/src/test/compatibility_test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function setupKits<
2626
// TODO(aomarks) See TODO about `any` at {@link NodeFactoryFromDefinition}.
2727
//
2828
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29-
Definition<any, any, any, any, any, any, any, any>
29+
Definition<any, any, any, any, any, any, any, any, any>
3030
>,
3131
>(definitions: DEFS) {
3232
const ctr = new KitBuilder({ url: "N/A" }).build(definitions);

packages/build/src/test/define_test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { BreadboardError } from "../internal/common/error.js";
1919
test("mono/mono", async () => {
2020
const values = { si1: "foo", si2: 123 };
2121

22-
// $ExpectType Definition<{ si1: string; si2: number; }, { so1: boolean; so2: null; }, undefined, undefined, never, false, false, false>
22+
// $ExpectType Definition<{ si1: string; si2: number; }, { so1: boolean; so2: null; }, undefined, undefined, never, false, false, false, { si1: { board: false; }; si2: { board: false; }; }>
2323
const d = defineNodeType({
2424
name: "foo",
2525
inputs: {
@@ -139,7 +139,7 @@ test("mono/mono", async () => {
139139
test("poly/mono", async () => {
140140
const values = { si1: "si1", di1: 1, di2: 2 };
141141

142-
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, undefined, never, false, false, false>
142+
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, undefined, never, false, false, false, { si1: { board: false; }; }>
143143
const d = defineNodeType({
144144
name: "foo",
145145
inputs: {
@@ -287,7 +287,7 @@ test("poly/mono", async () => {
287287
test("mono/poly", async () => {
288288
const values = { si1: "si1" };
289289

290-
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, undefined, number, never, false, false, false>
290+
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, undefined, number, never, false, false, false, { si1: { board: false; }; }>
291291
const d = defineNodeType({
292292
name: "foo",
293293
inputs: {
@@ -390,7 +390,7 @@ test("mono/poly", async () => {
390390
test("poly/poly", async () => {
391391
const values = { si1: "si1", di1: 1, di2: 2 };
392392

393-
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, number, never, false, false, false>
393+
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, number, never, false, false, false, { si1: { board: false; }; }>
394394
const d = defineNodeType({
395395
name: "foo",
396396
inputs: {
@@ -573,7 +573,7 @@ test("async invoke function", async () => {
573573
test("reflective", async () => {
574574
const values = { si1: "si1", di1: 1, di2: 2 };
575575

576-
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, string, never, true, false, false>
576+
// $ExpectType Definition<{ si1: string; }, { so1: boolean; }, number, string, never, true, false, false, { si1: { board: false; }; }>
577577
const d = defineNodeType({
578578
name: "foo",
579579
inputs: {
@@ -708,7 +708,7 @@ test("reflective", async () => {
708708
test("primary input with no other inputs", () => {
709709
const values = { si1: 123 };
710710

711-
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, "si1", false>
711+
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, "si1", false, { si1: { board: false; }; }>
712712
const d = defineNodeType({
713713
name: "foo",
714714
inputs: {
@@ -772,7 +772,7 @@ test("primary input with no other inputs", () => {
772772
test("primary input with another input", () => {
773773
const values = { si1: 123, si2: true };
774774

775-
// $ExpectType Definition<{ si1: number; si2: boolean; }, { so1: boolean; }, undefined, undefined, never, false, "si1", false>
775+
// $ExpectType Definition<{ si1: number; si2: boolean; }, { so1: boolean; }, undefined, undefined, never, false, "si1", false, { si1: { board: false; }; si2: { board: false; }; }>
776776
const d = defineNodeType({
777777
name: "foo",
778778
inputs: {
@@ -835,7 +835,7 @@ test("primary input with another input", () => {
835835
});
836836

837837
test("primary output with no other outputs", () => {
838-
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, false, "so1">
838+
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, false, "so1", { si1: { board: false; }; }>
839839
const d = defineNodeType({
840840
name: "foo",
841841
inputs: {
@@ -897,7 +897,7 @@ test("primary output with no other outputs", () => {
897897
});
898898

899899
test("primary output with other outputs", () => {
900-
// $ExpectType Definition<{ si1: number; }, { so1: boolean; so2: number; }, undefined, undefined, never, false, false, "so1">
900+
// $ExpectType Definition<{ si1: number; }, { so1: boolean; so2: number; }, undefined, undefined, never, false, false, "so1", { si1: { board: false; }; }>
901901
const d = defineNodeType({
902902
name: "foo",
903903
inputs: {
@@ -961,7 +961,7 @@ test("primary output with other outputs", () => {
961961
});
962962

963963
test("primary input + output", () => {
964-
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, "si1", "so1">
964+
// $ExpectType Definition<{ si1: number; }, { so1: boolean; }, undefined, undefined, never, false, "si1", "so1", { si1: { board: false; }; }>
965965
const d = defineNodeType({
966966
name: "foo",
967967
inputs: {

0 commit comments

Comments
 (0)