From 413d21e617e3193fcd101c8be422de45f475556b Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Tue, 10 Dec 2024 21:53:38 +0900 Subject: [PATCH 01/28] refactor(graph): Convert SubGraph sets to arrays and update version This updates the SubGraph interface to use arrays instead of Sets for nodes and connections, making the data structure more serializable. Also bumps graph version to 2024-12-11. - Change SubGraph.nodes from Set to NodeId[] - Change SubGraph.connections from Set to ConnectionId[] - Update tests to check array lengths instead of Set sizes - Add migration logic for 2024-12-10 -> 2024-12-11 - Update default graph version in initGraph() BREAKING CHANGE: SubGraph interface now uses arrays instead of Sets --- .../p/[agentId]/canary/contexts/graph.tsx | 5 +++++ .../p/[agentId]/canary/graph.test.ts | 4 ++-- app/(playground)/p/[agentId]/canary/graph.ts | 16 ++++++++++++---- app/(playground)/p/[agentId]/canary/types.ts | 8 ++++---- app/(playground)/p/[agentId]/canary/utils.ts | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx index a487800a..4eecf694 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx @@ -9,6 +9,7 @@ import { useRef, useState, } from "react"; +import { deriveSubGraphs } from "../graph"; import type { Artifact, Connection, @@ -116,6 +117,10 @@ function applyActions( for (const action of actions) { currentGraph = graphReducer(currentGraph, action); } + currentGraph = { + ...currentGraph, + subGraphs: deriveSubGraphs(currentGraph), + }; return currentGraph; } diff --git a/app/(playground)/p/[agentId]/canary/graph.test.ts b/app/(playground)/p/[agentId]/canary/graph.test.ts index e9dc78aa..078ddf2d 100644 --- a/app/(playground)/p/[agentId]/canary/graph.test.ts +++ b/app/(playground)/p/[agentId]/canary/graph.test.ts @@ -129,8 +129,8 @@ describe("deriveSubGraphs", () => { expect(subGraphs.length).toBe(2); }); test("one subgraph has two nodes while the other has three", () => { - expect(subGraphs[0].nodes.size).toBe(2); - expect(subGraphs[1].nodes.size).toBe(3); + expect(subGraphs[0].nodes.length).toBe(2); + expect(subGraphs[1].nodes.length).toBe(3); }); }); diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index 2ddf2776..bdebf35b 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -72,8 +72,8 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { const subGraph: SubGraph = { id: createSubgraphId(), name: `SubGraph ${subGraphs.length + 1}`, - nodes: connectedNodes, - connections: subGraphConnections, + nodes: Array.from(connectedNodes), + connections: Array.from(subGraphConnections), }; subGraphs.push(subGraph); @@ -85,8 +85,8 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { const subGraph: SubGraph = { id: createSubgraphId(), name: `SubGraph ${subGraphs.length + 1}`, - nodes: new Set([node.id]), - connections: new Set(), + nodes: [node.id], + connections: [], }; subGraphs.push(subGraph); @@ -137,5 +137,13 @@ export function migrateGraph(graph: Graph): Graph { }; } + if (newGraph.version === "2024-12-10") { + newGraph = { + ...newGraph, + version: "2024-12-11", + subGraphs: deriveSubGraphs(newGraph), + }; + } + return newGraph; } diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index 3a23a5c4..14ac41c1 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -166,8 +166,8 @@ interface TextStreamArtifact extends StreamAtrifact { export type Artifact = TextArtifact | TextStreamArtifact; export type GraphId = `grph_${string}`; -type GraphVersion = "2024-12-09" | "2024-12-10"; -export type LatestGraphVersion = "2024-12-10"; +type GraphVersion = "2024-12-09" | "2024-12-10" | "2024-12-11"; +export type LatestGraphVersion = "2024-12-11"; export interface Graph { id: GraphId; nodes: Node[]; @@ -209,8 +209,8 @@ export type SubGraphId = `sbgrph_${string}`; export interface SubGraph { id: SubGraphId; name: string; - nodes: Set; - connections: Set; + nodes: NodeId[]; + connections: ConnectionId[]; } export type AgentId = `agnt_${string}`; diff --git a/app/(playground)/p/[agentId]/canary/utils.ts b/app/(playground)/p/[agentId]/canary/utils.ts index 474d12bf..0cc8e5ab 100644 --- a/app/(playground)/p/[agentId]/canary/utils.ts +++ b/app/(playground)/p/[agentId]/canary/utils.ts @@ -149,7 +149,7 @@ export function initGraph(): Graph { nodes: [], connections: [], artifacts: [], - version: "2024-12-10" satisfies LatestGraphVersion, + version: "2024-12-11" satisfies LatestGraphVersion, subGraphs: [], }; } From 00c0b5a368a2a11efed74988ae94ea82ba1dc8da Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Tue, 10 Dec 2024 22:01:32 +0900 Subject: [PATCH 02/28] feat(ui): Add structure panel showing subgraph hierarchy Adds a new navigation tab that displays the hierarchical structure of subgraphs and their constituent nodes, providing better visualization of graph organization. - Add Structure component with subgraph and node listing - Include ListTree icon for structure tab navigation - Display subgraph names with nested node lists --- .../canary/components/navigation-panel.tsx | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index a0ab800e..7425b392 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -1,11 +1,18 @@ import * as Tabs from "@radix-ui/react-tabs"; import { getDownloadUrl, head } from "@vercel/blob"; -import { DownloadIcon, GithubIcon, HammerIcon, XIcon } from "lucide-react"; +import { + DownloadIcon, + GithubIcon, + HammerIcon, + ListTreeIcon, + XIcon, +} from "lucide-react"; import { type ComponentProps, type ReactNode, createContext, useContext, + useMemo, useState, } from "react"; import { LayersIcon } from "../../beta-proto/components/icons/layers"; @@ -68,6 +75,9 @@ export function NavigationPanel() { {/* */} + + + {developerMode && ( @@ -81,6 +91,9 @@ export function NavigationPanel() { {/* */} + + + @@ -186,3 +199,32 @@ function Developer() { ); } + +export function Structure() { + const { graph } = useGraph(); + const subGraphs = useMemo( + () => + graph.subGraphs.map((subGraph) => ({ + ...subGraph, + nodes: subGraph.nodes + .map((nodeId) => graph.nodes.find((node) => node.id === nodeId)) + .filter((node) => node !== undefined), + })), + [graph], + ); + return ( + + Structure +
+ {subGraphs.map((subGraph) => ( +
+

{subGraph.name}

+
+ {subGraph.nodes.map((node) => node.name)} +
+
+ ))} +
+
+ ); +} From 9781cd5673eb04e94c430c16035b156a431d3c87 Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Wed, 11 Dec 2024 08:52:41 +0900 Subject: [PATCH 03/28] fix(navigation-panel): Improve display of subGraph nodes - Update rendering logic for subGraph nodes to include unique keys for each node, ensuring proper reconciliation in React. - Adjust styling for better visual hierarchy and readability. --- .../p/[agentId]/canary/components/navigation-panel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index 7425b392..2cd98dc4 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -219,8 +219,10 @@ export function Structure() { {subGraphs.map((subGraph) => (

{subGraph.name}

-
- {subGraph.nodes.map((node) => node.name)} +
+ {subGraph.nodes.map((node) => ( +
{node.name}
+ ))}
))} From 522fbf4304418758aeaad3b904a6652303445693 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Wed, 11 Dec 2024 16:43:51 +0900 Subject: [PATCH 04/28] feat(graph): Add job sequencing and improve structure visualization Create job sequences from subgraphs using topological sorting and enhance the navigation panel UI with content type icons. Changes include: - Implement topological sort algorithm to convert graphs into sequential jobs - Add job and step types with unique ID generation - Update SubGraph interface to include job sequences - Enhance navigation panel with icons and improved styling The job sequencing allows parallel execution of independent nodes while maintaining dependencies. Each subgraph now contains an ordered list of jobs where steps within a job can be executed concurrently. --- .../canary/components/navigation-panel.tsx | 20 ++- app/(playground)/p/[agentId]/canary/graph.ts | 161 +++++++++++++++++- app/(playground)/p/[agentId]/canary/types.ts | 12 ++ app/(playground)/p/[agentId]/canary/utils.ts | 10 ++ 4 files changed, 198 insertions(+), 5 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index 2cd98dc4..43647ba8 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -16,9 +16,11 @@ import { useState, } from "react"; import { LayersIcon } from "../../beta-proto/components/icons/layers"; +import { WilliIcon } from "../../beta-proto/components/icons/willi"; import { useAgentName } from "../contexts/agent-name"; import { useDeveloperMode } from "../contexts/developer-mode"; import { useGraph } from "../contexts/graph"; +import { ContentTypeIcon } from "./content-type-icon"; function TabsTrigger( props: Omit, "className">, @@ -218,10 +220,22 @@ export function Structure() {
{subGraphs.map((subGraph) => (
-

{subGraph.name}

-
+
+ +

{subGraph.name}

+
+
{subGraph.nodes.map((node) => ( -
{node.name}
+
+ + {node.name} +
))}
diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index bdebf35b..7d1a8184 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -1,5 +1,14 @@ -import type { ConnectionId, Files, Graph, NodeId, SubGraph } from "./types"; -import { createSubgraphId } from "./utils"; +import type { + Connection, + ConnectionId, + Files, + Graph, + Job, + NodeId, + Step, + SubGraph, +} from "./types"; +import { createJobId, createStepId, createSubgraphId } from "./utils"; export function deriveSubGraphs(graph: Graph): SubGraph[] { const processedNodes = new Set(); @@ -60,6 +69,147 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { return subGraphConnections; } + /** + * Converts a directed graph into a sequence of jobs with steps based on topological sorting. + * + * Input Graph: Output Jobs: + * A → B → D Job1: [A] + * ↓ ↓ Job2: [B, C] + * C → E Job3: [D, E] + * + * @param graph The input graph with nodes and connections + * @returns Array of jobs where each job contains steps that can be executed in parallel + */ + function createJobsFromGraph( + nodeIds: NodeId[], + connections: Connection[], + ): Job[] { + /** + * Calculates the number of incoming edges for each node. + * + * Example: + * A → B → C + * ↓ ↓ + * D → E + * + * Results: + * A: 0 + * B: 1 + * C: 1 + * D: 1 + * E: 2 + */ + const calculateInDegrees = ( + nodeIds: NodeId[], + connections: Connection[], + ): Map => { + const inDegrees = new Map(); + + for (const nodeId of nodeIds) { + inDegrees.set(nodeId, 0); + } + + for (const conn of connections) { + const currentDegree = inDegrees.get(conn.targetNodeId) || 0; + inDegrees.set(conn.targetNodeId, currentDegree + 1); + } + + return inDegrees; + }; + + /** + * Gets all direct child nodes of a given node. + * + * Example: + * For node A in: + * A → B + * ↓ + * C + * + * Returns: [B, C] + */ + const getChildNodes = ( + nodeId: NodeId, + connections: Connection[], + ): string[] => { + return connections + .filter((conn) => conn.sourceNodeId === nodeId) + .map((conn) => conn.targetNodeId); + }; + + /** + * Performs topological sort and groups nodes by levels. + * + * Example graph: Result levels: + * A → B → D Level 1: [A] + * ↓ ↓ Level 2: [B, C] + * C → E Level 3: [D, E] + * + * Each level contains nodes that can be processed in parallel + */ + const topologicalSort = ( + nodeIds: NodeId[], + connections: Connection[], + ): NodeId[][] => { + const inDegrees = calculateInDegrees(nodeIds, connections); + const levels: NodeId[][] = []; + let currentLevel: NodeId[] = []; + + for (const nodeId of nodeIds) { + if (inDegrees.get(nodeId) === 0) { + currentLevel.push(nodeId); + } + } + + while (currentLevel.length > 0) { + levels.push([...currentLevel]); + const nextLevel: NodeId[] = []; + + for (const nodeId of currentLevel) { + const children = getChildNodes(nodeId, connections); + for (const childId of children) { + const newDegree = (inDegrees.get(childId) || 0) - 1; + inDegrees.set(childId, newDegree); + if (newDegree === 0) { + nextLevel.push(childId as NodeId); + } + } + } + + currentLevel = nextLevel; + } + + return levels; + }; + + /** + * Converts topologically sorted levels into job structures. + * + * Input levels: Output jobs: + * [[A], [B,C], [D]] [{jobId: 1, steps: [{nodeId: "A"}]}, + * {jobId: 2, steps: [{nodeId: "B"}, {nodeId: "C"}]}, + * {jobId: 3, steps: [{nodeId: "D"}]}] + */ + function createJobs(levels: NodeId[][]): Job[] { + return levels.map( + (level) => + ({ + id: createJobId(), + steps: level.map( + (nodeId) => + ({ + nodeId, + id: createStepId(), + variableNodeIds: [], + }) satisfies Step, + ), + }) satisfies Job, + ); + } + + const levels = topologicalSort(nodeIds, connections); + return createJobs(levels); + } for (const node of graph.nodes) { if (processedNodes.has(node.id)) continue; @@ -74,6 +224,12 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { name: `SubGraph ${subGraphs.length + 1}`, nodes: Array.from(connectedNodes), connections: Array.from(subGraphConnections), + jobs: createJobsFromGraph( + Array.from(connectedNodes), + graph.connections.filter((connection) => + subGraphConnections.has(connection.id), + ), + ), }; subGraphs.push(subGraph); @@ -87,6 +243,7 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { name: `SubGraph ${subGraphs.length + 1}`, nodes: [node.id], connections: [], + jobs: createJobsFromGraph([node.id], []), }; subGraphs.push(subGraph); diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index 14ac41c1..ff1b34ff 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -206,9 +206,21 @@ export type Tool = export type SubGraphId = `sbgrph_${string}`; +export type StepId = `stp_${string}`; +export interface Step { + id: StepId; + nodeId: NodeId; + variableNodeIds: NodeId[]; +} +export type JobId = `jb_${string}`; +export interface Job { + id: JobId; + steps: Step[]; +} export interface SubGraph { id: SubGraphId; name: string; + jobs: Job[]; nodes: NodeId[]; connections: ConnectionId[]; } diff --git a/app/(playground)/p/[agentId]/canary/utils.ts b/app/(playground)/p/[agentId]/canary/utils.ts index 0cc8e5ab..c18cd637 100644 --- a/app/(playground)/p/[agentId]/canary/utils.ts +++ b/app/(playground)/p/[agentId]/canary/utils.ts @@ -8,10 +8,12 @@ import type { Files, Graph, GraphId, + JobId, LatestGraphVersion, Node, NodeHandleId, NodeId, + StepId, SubGraphId, Text, TextGenerateActionContent, @@ -45,6 +47,14 @@ export function createSubgraphId(): SubGraphId { return `sbgrph_${createId()}`; } +export function createJobId(): JobId { + return `jb_${createId()}`; +} + +export function createStepId(): StepId { + return `stp_${createId()}`; +} + export function isTextGeneration(node: Node): node is TextGeneration { return node.content.type === "textGeneration"; } From a7538037ae5f034dd8bd2dda48ac4d90c91482a6 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Wed, 11 Dec 2024 17:34:07 +0900 Subject: [PATCH 05/28] refactor(navigation): Improve structure panel with subflow grouping - Reorganize structure view to display nodes within job-based subflows - Replace "SubGraph" naming with more user-friendly "Flow" terminology - Add visual hierarchy for multi-step jobs with proper indentation - Create reusable StructureNodeItem component for consistent node display The changes enhance navigation clarity by grouping related nodes into logical flows and providing better visual organization of the graph structure. --- .../canary/components/navigation-panel.tsx | 73 +++++++++++++++---- app/(playground)/p/[agentId]/canary/graph.ts | 2 +- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index 43647ba8..7b3b0f92 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -1,7 +1,9 @@ import * as Tabs from "@radix-ui/react-tabs"; import { getDownloadUrl, head } from "@vercel/blob"; +import clsx from "clsx/lite"; import { DownloadIcon, + FrameIcon, GithubIcon, HammerIcon, ListTreeIcon, @@ -20,6 +22,7 @@ import { WilliIcon } from "../../beta-proto/components/icons/willi"; import { useAgentName } from "../contexts/agent-name"; import { useDeveloperMode } from "../contexts/developer-mode"; import { useGraph } from "../contexts/graph"; +import type { Node } from "../types"; import { ContentTypeIcon } from "./content-type-icon"; function TabsTrigger( @@ -202,15 +205,46 @@ function Developer() { ); } +function StructureNodeItem({ + node, + className, +}: { node: Node; className?: string }) { + return ( +
+ +

{node.name}

+
+ ); +} export function Structure() { const { graph } = useGraph(); const subGraphs = useMemo( () => graph.subGraphs.map((subGraph) => ({ ...subGraph, - nodes: subGraph.nodes - .map((nodeId) => graph.nodes.find((node) => node.id === nodeId)) - .filter((node) => node !== undefined), + jobs: subGraph.jobs.map((job) => ({ + ...job, + steps: job.steps + .map((step) => { + const node = graph.nodes.find((node) => node.id === step.nodeId); + if (node === undefined) { + return null; + } + return { + ...step, + node, + }; + }) + .filter((step) => step !== null), + })), })), [graph], ); @@ -225,16 +259,29 @@ export function Structure() {

{subGraph.name}

- {subGraph.nodes.map((node) => ( -
- - {node.name} + {subGraph.jobs.map((job) => ( +
+ {job.steps.length === 1 ? ( + + ) : ( +
+
+ +

Subflow

+
+ + {job.steps.map((step) => ( + + ))} +
+ )}
))}
diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index 7d1a8184..384e2f23 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -221,7 +221,7 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { const subGraph: SubGraph = { id: createSubgraphId(), - name: `SubGraph ${subGraphs.length + 1}`, + name: `Flow ${subGraphs.length + 1}`, nodes: Array.from(connectedNodes), connections: Array.from(subGraphConnections), jobs: createJobsFromGraph( From cf00ce21531f50b043deb681a196a08a092a387c Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Wed, 11 Dec 2024 18:58:19 +0900 Subject: [PATCH 06/28] feat(graph): Add variable node support in graph structure Enhance graph processing to handle variable nodes alongside action nodes: - Include variable nodes in step structure with proper associations - Update UI to display variable nodes under their parent action nodes - Filter graph traversal to only consider action nodes for job creation - Add variableNodeIds resolution for each step in the graph This change improves visualization of data dependencies while maintaining the existing action node execution flow. --- .../canary/components/navigation-panel.tsx | 43 ++++++++++-- app/(playground)/p/[agentId]/canary/graph.ts | 69 +++++++++++-------- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index 7b3b0f92..a2df8b44 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -22,7 +22,7 @@ import { WilliIcon } from "../../beta-proto/components/icons/willi"; import { useAgentName } from "../contexts/agent-name"; import { useDeveloperMode } from "../contexts/developer-mode"; import { useGraph } from "../contexts/graph"; -import type { Node } from "../types"; +import type { Node, Step } from "../types"; import { ContentTypeIcon } from "./content-type-icon"; function TabsTrigger( @@ -224,6 +224,29 @@ function StructureNodeItem({
); } + +function StructureStepItem({ + step, + stepClassName, + variableNodesClassName, +}: { + step: Step & { node: Node; variableNodes: Node[] }; + stepClassName?: string; + variableNodesClassName?: string; +}) { + return ( +
+ + {step.variableNodes.map((node) => ( + + ))} +
+ ); +} export function Structure() { const { graph } = useGraph(); const subGraphs = useMemo( @@ -235,12 +258,16 @@ export function Structure() { steps: job.steps .map((step) => { const node = graph.nodes.find((node) => node.id === step.nodeId); + const variableNodes = step.variableNodeIds + .map((nodeId) => graph.nodes.find((node) => node.id === nodeId)) + .filter((node) => node !== undefined); if (node === undefined) { return null; } return { ...step, node, + variableNodes, }; }) .filter((step) => step !== null), @@ -262,9 +289,10 @@ export function Structure() { {subGraph.jobs.map((job) => (
{job.steps.length === 1 ? ( - ) : (
@@ -274,10 +302,11 @@ export function Structure() {
{job.steps.map((step) => ( - ))}
diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index 384e2f23..65abc14e 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -4,6 +4,7 @@ import type { Files, Graph, Job, + Node, NodeId, Step, SubGraph, @@ -81,7 +82,7 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { * @returns Array of jobs where each job contains steps that can be executed in parallel */ function createJobsFromGraph( - nodeIds: NodeId[], + nodes: Node[], connections: Connection[], ): Job[] { /** @@ -182,36 +183,48 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { return levels; }; - /** - * Converts topologically sorted levels into job structures. - * - * Input levels: Output jobs: - * [[A], [B,C], [D]] [{jobId: 1, steps: [{nodeId: "A"}]}, - * {jobId: 2, steps: [{nodeId: "B"}, {nodeId: "C"}]}, - * {jobId: 3, steps: [{nodeId: "D"}]}] - */ - function createJobs(levels: NodeId[][]): Job[] { - return levels.map( - (level) => - ({ - id: createJobId(), - steps: level.map( - (nodeId) => - ({ - nodeId, - id: createStepId(), - variableNodeIds: [], - }) satisfies Step, - ), - }) satisfies Job, + function resolveVariableNodeIds(nodeId: NodeId) { + const variableConnections = new Set( + connections + .filter( + (connection) => + connection.targetNodeId === nodeId && + connection.sourceNodeType === "variable", + ) + .map((connection) => connection.sourceNodeId), ); + const variableNodes = nodes.filter((node) => + variableConnections.has(node.id), + ); + return variableNodes.map((node) => node.id); } - const levels = topologicalSort(nodeIds, connections); - return createJobs(levels); + const actionNodeIds = nodes + .filter((node) => node.type === "action") + .map((node) => node.id); + const levels = topologicalSort( + actionNodeIds, + connections.filter( + (connection) => connection.sourceNodeType === "action", + ), + ); + return levels.map( + (level) => + ({ + id: createJobId(), + steps: level.map( + (nodeId) => + ({ + nodeId, + id: createStepId(), + variableNodeIds: resolveVariableNodeIds(nodeId), + }) satisfies Step, + ), + }) satisfies Job, + ); } - for (const node of graph.nodes) { + for (const node of graph.nodes.filter((node) => node.type === "action")) { if (processedNodes.has(node.id)) continue; const connectedNodes = findConnectedComponent(node.id); @@ -225,7 +238,7 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { nodes: Array.from(connectedNodes), connections: Array.from(subGraphConnections), jobs: createJobsFromGraph( - Array.from(connectedNodes), + graph.nodes.filter((node) => connectedNodes.has(node.id)), graph.connections.filter((connection) => subGraphConnections.has(connection.id), ), @@ -243,7 +256,7 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { name: `SubGraph ${subGraphs.length + 1}`, nodes: [node.id], connections: [], - jobs: createJobsFromGraph([node.id], []), + jobs: createJobsFromGraph([node], []), }; subGraphs.push(subGraph); From 91950c384d1e47617450901f4665f32a6685249e Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Wed, 11 Dec 2024 21:30:18 +0900 Subject: [PATCH 07/28] refactor(graph): Rename SubGraph to Flow for better semantics Replaces all instances of "SubGraph" with "Flow" to better represent the concept of execution flow in the graph system. Updates include: - Rename type SubGraphId to FlowId - Update Graph interface to use flows instead of subGraphs - Refactor deriveSubGraphs to deriveFlows - Bump version to 20241212 format - Add cursor-default to navigation panel items No breaking changes as this is an internal refactor pre-release. --- .../canary/components/navigation-panel.tsx | 18 +++---- .../p/[agentId]/canary/contexts/graph.tsx | 4 +- .../p/[agentId]/canary/graph.test.ts | 18 +++---- app/(playground)/p/[agentId]/canary/graph.ts | 50 +++++++++---------- app/(playground)/p/[agentId]/canary/types.ts | 12 ++--- app/(playground)/p/[agentId]/canary/utils.ts | 10 ++-- 6 files changed, 56 insertions(+), 56 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx index a2df8b44..337e8111 100644 --- a/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/navigation-panel.tsx @@ -212,7 +212,7 @@ function StructureNodeItem({ return (
@@ -249,11 +249,11 @@ function StructureStepItem({ } export function Structure() { const { graph } = useGraph(); - const subGraphs = useMemo( + const flows = useMemo( () => - graph.subGraphs.map((subGraph) => ({ - ...subGraph, - jobs: subGraph.jobs.map((job) => ({ + graph.flows.map((flow) => ({ + ...flow, + jobs: flow.jobs.map((job) => ({ ...job, steps: job.steps .map((step) => { @@ -279,14 +279,14 @@ export function Structure() { Structure
- {subGraphs.map((subGraph) => ( -
+ {flows.map((flow) => ( +
-

{subGraph.name}

+

{flow.name}

- {subGraph.jobs.map((job) => ( + {flow.jobs.map((job) => (
{job.steps.length === 1 ? ( { - const subGraphs = deriveSubGraphs(graph); - test("two sub graphs", () => { - expect(subGraphs.length).toBe(2); +describe("deriveFlows", () => { + const flows = deriveFlows(graph); + test("two sub flows", () => { + expect(flows.length).toBe(2); }); - test("one subgraph has two nodes while the other has three", () => { - expect(subGraphs[0].nodes.length).toBe(2); - expect(subGraphs[1].nodes.length).toBe(3); + test("one flow has two nodes while the other has three", () => { + expect(flows[0].nodes.length).toBe(2); + expect(flows[1].nodes.length).toBe(3); }); }); diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index 65abc14e..e8570bf3 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -2,18 +2,18 @@ import type { Connection, ConnectionId, Files, + Flow, Graph, Job, Node, NodeId, Step, - SubGraph, } from "./types"; -import { createJobId, createStepId, createSubgraphId } from "./utils"; +import { createFlowId, createJobId, createStepId } from "./utils"; -export function deriveSubGraphs(graph: Graph): SubGraph[] { +export function deriveFlows(graph: Graph): Flow[] { const processedNodes = new Set(); - const subGraphs: SubGraph[] = []; + const flows: Flow[] = []; const connectionMap = new Map>(); for (const connection of graph.connections) { @@ -56,19 +56,19 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { return connectedNodes; } - function findSubGraphConnections(nodes: Set): Set { - const subGraphConnections = new Set(); + function findFlowConnections(nodes: Set): Set { + const flowConnections = new Set(); for (const connection of graph.connections) { if ( nodes.has(connection.sourceNodeId) && nodes.has(connection.targetNodeId) ) { - subGraphConnections.add(connection.id); + flowConnections.add(connection.id); } } - return subGraphConnections; + return flowConnections; } /** * Converts a directed graph into a sequence of jobs with steps based on topological sorting. @@ -230,45 +230,45 @@ export function deriveSubGraphs(graph: Graph): SubGraph[] { const connectedNodes = findConnectedComponent(node.id); if (connectedNodes.size > 0) { - const subGraphConnections = findSubGraphConnections(connectedNodes); + const flowConnections = findFlowConnections(connectedNodes); - const subGraph: SubGraph = { - id: createSubgraphId(), - name: `Flow ${subGraphs.length + 1}`, + const flow: Flow = { + id: createFlowId(), + name: `Flow ${flows.length + 1}`, nodes: Array.from(connectedNodes), - connections: Array.from(subGraphConnections), + connections: Array.from(flowConnections), jobs: createJobsFromGraph( graph.nodes.filter((node) => connectedNodes.has(node.id)), graph.connections.filter((connection) => - subGraphConnections.has(connection.id), + flowConnections.has(connection.id), ), ), }; - subGraphs.push(subGraph); + flows.push(flow); for (const nodeId of connectedNodes) { processedNodes.add(nodeId); } } else { - const subGraph: SubGraph = { - id: createSubgraphId(), - name: `SubGraph ${subGraphs.length + 1}`, + const flow: Flow = { + id: createFlowId(), + name: `Flow ${flows.length + 1}`, nodes: [node.id], connections: [], jobs: createJobsFromGraph([node], []), }; - subGraphs.push(subGraph); + flows.push(flow); processedNodes.add(node.id); } } - return subGraphs; + return flows; } export function isLatestVersion(graph: Graph): boolean { - return graph.version === "2024-12-10"; + return graph.version === "20241212"; } export function migrateGraph(graph: Graph): Graph { @@ -278,7 +278,7 @@ export function migrateGraph(graph: Graph): Graph { newGraph = { ...newGraph, version: "2024-12-09", - subGraphs: deriveSubGraphs(newGraph), + flows: deriveFlows(newGraph), }; } @@ -307,11 +307,11 @@ export function migrateGraph(graph: Graph): Graph { }; } - if (newGraph.version === "2024-12-10") { + if (newGraph.version === "2024-12-10" || newGraph.version === "2024-12-11") { newGraph = { ...newGraph, - version: "2024-12-11", - subGraphs: deriveSubGraphs(newGraph), + version: "20241212", + flows: deriveFlows(newGraph), }; } diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index ff1b34ff..ae164260 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -166,15 +166,15 @@ interface TextStreamArtifact extends StreamAtrifact { export type Artifact = TextArtifact | TextStreamArtifact; export type GraphId = `grph_${string}`; -type GraphVersion = "2024-12-09" | "2024-12-10" | "2024-12-11"; -export type LatestGraphVersion = "2024-12-11"; +type GraphVersion = "2024-12-09" | "2024-12-10" | "2024-12-11" | "20241212"; +export type LatestGraphVersion = "20241212"; export interface Graph { id: GraphId; nodes: Node[]; connections: Connection[]; artifacts: Artifact[]; version: GraphVersion; - subGraphs: SubGraph[]; + flows: Flow[]; } interface ToolBase { @@ -204,7 +204,7 @@ export type Tool = | AddTextGenerationNodeTool | MoveTool; -export type SubGraphId = `sbgrph_${string}`; +export type FlowId = `flw_${string}`; export type StepId = `stp_${string}`; export interface Step { @@ -217,8 +217,8 @@ export interface Job { id: JobId; steps: Step[]; } -export interface SubGraph { - id: SubGraphId; +export interface Flow { + id: FlowId; name: string; jobs: Job[]; nodes: NodeId[]; diff --git a/app/(playground)/p/[agentId]/canary/utils.ts b/app/(playground)/p/[agentId]/canary/utils.ts index c18cd637..7cddbc37 100644 --- a/app/(playground)/p/[agentId]/canary/utils.ts +++ b/app/(playground)/p/[agentId]/canary/utils.ts @@ -6,6 +6,7 @@ import type { File, FileId, Files, + FlowId, Graph, GraphId, JobId, @@ -14,7 +15,6 @@ import type { NodeHandleId, NodeId, StepId, - SubGraphId, Text, TextGenerateActionContent, TextGeneration, @@ -43,8 +43,8 @@ export function createFileId(): FileId { return `fl_${createId()}`; } -export function createSubgraphId(): SubGraphId { - return `sbgrph_${createId()}`; +export function createFlowId(): FlowId { + return `flw_${createId()}`; } export function createJobId(): JobId { @@ -159,8 +159,8 @@ export function initGraph(): Graph { nodes: [], connections: [], artifacts: [], - version: "2024-12-11" satisfies LatestGraphVersion, - subGraphs: [], + version: "20241212" satisfies LatestGraphVersion, + flows: [], }; } From 97af62267a3a7462a347cb0d91319e3f511096f4 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Wed, 11 Dec 2024 21:54:57 +0900 Subject: [PATCH 08/28] feat(ui): Add header component to playground editor Add new header with navigation and mode switching controls that includes: - Logo and playground title - Edit/View mode toggle buttons with selection indicator - Basic layout structure for future run controls The header provides better navigation context and mode management for the playground editor interface. --- .../p/[agentId]/canary/components/editor.tsx | 5 ++ .../p/[agentId]/canary/components/header.tsx | 84 +++++++++++++++++++ .../canary/components/icons/sparkles.tsx | 26 ++++++ 3 files changed, 115 insertions(+) create mode 100644 app/(playground)/p/[agentId]/canary/components/header.tsx create mode 100644 app/(playground)/p/[agentId]/canary/components/icons/sparkles.tsx diff --git a/app/(playground)/p/[agentId]/canary/components/editor.tsx b/app/(playground)/p/[agentId]/canary/components/editor.tsx index 687b1329..03299f9d 100644 --- a/app/(playground)/p/[agentId]/canary/components/editor.tsx +++ b/app/(playground)/p/[agentId]/canary/components/editor.tsx @@ -20,6 +20,7 @@ import { useToolbar } from "../contexts/toolbar"; import type { NodeId, Tool } from "../types"; import { createNodeId, isTextGeneration } from "../utils"; import { Edge } from "./edge"; +import { Header } from "./header"; import { KeyboardShortcut } from "./keyboard-shortcut"; import { NavigationPanel } from "./navigation-panel"; import { Node, PreviewNode } from "./node"; @@ -297,6 +298,10 @@ export function Editor() { backgroundSize: "cover", }} /> + + +
+ diff --git a/app/(playground)/p/[agentId]/canary/components/header.tsx b/app/(playground)/p/[agentId]/canary/components/header.tsx new file mode 100644 index 00000000..c6f3fd27 --- /dev/null +++ b/app/(playground)/p/[agentId]/canary/components/header.tsx @@ -0,0 +1,84 @@ +import { GiselleLogo } from "@/components/giselle-logo"; +import Link from "next/link"; +import type { ReactNode } from "react"; +// import { RunButton } from "./flow/components/run-button"; +import { SparklesIcon } from "./icons/sparkles"; + +function SelectionIndicator() { + return ( + + Selection Indicator + + + ); +} + +interface ModeButtonProps { + children: ReactNode; + mode: "edit" | "view"; + selected?: boolean; +} +function ModeButton(props: ModeButtonProps) { + const mode = "edit"; + return ( + + ); +} + +export function Header() { + return ( +
+
+ + + +
Playground
+ {/** +
+
+
+ edit + view +
+
+ {/*
+ +
*/} +
+ ); +} diff --git a/app/(playground)/p/[agentId]/canary/components/icons/sparkles.tsx b/app/(playground)/p/[agentId]/canary/components/icons/sparkles.tsx new file mode 100644 index 00000000..d31d632e --- /dev/null +++ b/app/(playground)/p/[agentId]/canary/components/icons/sparkles.tsx @@ -0,0 +1,26 @@ +import type { FC, SVGProps } from "react"; + +export const SparklesIcon: FC> = (props) => ( + + Sparkles Icon + + + + + + + + + + + + + + +); From c65e65e67c72c668f7b83f73f4aa1e67b63a3f26 Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Wed, 11 Dec 2024 23:47:47 +0900 Subject: [PATCH 09/28] refactor(canary): Move utility functions to lib directory - Refactor the project structure to improve organization by moving utility functions from various components to a centralized `lib` directory. This allows for better separation of concerns and easier maintenance of utility functions. - Update import statements across the project to reflect the new file locations. No breaking changes introduced with this refactor. --- app/(main)/agents-v2/layout.tsx | 2 +- app/(main)/agents-v2/page.tsx | 2 +- .../p/[agentId]/canary/actions.ts | 14 +++++----- .../p/[agentId]/canary/components/editor.tsx | 2 +- .../canary/components/properties-panel.tsx | 28 +++++++++---------- .../p/[agentId]/canary/contexts/execution.tsx | 2 +- app/(playground)/p/[agentId]/canary/graph.ts | 2 +- .../[agentId]/canary/{ => lib}/utils.test.ts | 0 .../p/[agentId]/canary/{ => lib}/utils.ts | 4 +-- app/(playground)/p/[agentId]/canary/page.tsx | 2 +- 10 files changed, 29 insertions(+), 29 deletions(-) rename app/(playground)/p/[agentId]/canary/{ => lib}/utils.test.ts (100%) rename app/(playground)/p/[agentId]/canary/{ => lib}/utils.ts (99%) diff --git a/app/(main)/agents-v2/layout.tsx b/app/(main)/agents-v2/layout.tsx index 5d16b221..a2827a8d 100644 --- a/app/(main)/agents-v2/layout.tsx +++ b/app/(main)/agents-v2/layout.tsx @@ -6,7 +6,7 @@ import { createId } from "@paralleldrive/cuid2"; import { redirect } from "next/navigation"; import type { ReactNode } from "react"; import { putGraph } from "../../(playground)/p/[agentId]/canary/actions"; -import { initGraph } from "../../(playground)/p/[agentId]/canary/utils"; +import { initGraph } from "../../(playground)/p/[agentId]/canary/lib/utils"; import { CreateAgentButton } from "./components"; export default function Layout({ diff --git a/app/(main)/agents-v2/page.tsx b/app/(main)/agents-v2/page.tsx index ed47ad78..5ba761e1 100644 --- a/app/(main)/agents-v2/page.tsx +++ b/app/(main)/agents-v2/page.tsx @@ -3,7 +3,7 @@ import { fetchCurrentTeam } from "@/services/teams"; import { and, eq, isNotNull } from "drizzle-orm"; import Link from "next/link"; import { type ReactNode, Suspense } from "react"; -import { formatTimestamp } from "../../(playground)/p/[agentId]/canary/utils"; +import { formatTimestamp } from "../../(playground)/p/[agentId]/canary/lib/utils"; function DataList({ label, children }: { label: string; children: ReactNode }) { return ( diff --git a/app/(playground)/p/[agentId]/canary/actions.ts b/app/(playground)/p/[agentId]/canary/actions.ts index 940dc809..b68fdc90 100644 --- a/app/(playground)/p/[agentId]/canary/actions.ts +++ b/app/(playground)/p/[agentId]/canary/actions.ts @@ -15,6 +15,13 @@ import { UnstructuredClient } from "unstructured-client"; import { Strategy } from "unstructured-client/sdk/models/shared"; import * as v from "valibot"; import { vercelBlobFileFolder, vercelBlobGraphFolder } from "./constants"; +import { + buildGraphPath, + elementsToMarkdown, + langfuseModel, + pathJoin, + toErrorWithMessage, +} from "./lib/utils"; import { textGenerationPrompt } from "./prompts"; import type { AgentId, @@ -29,13 +36,6 @@ import type { TextArtifactObject, TextGenerateActionContent, } from "./types"; -import { - buildGraphPath, - elementsToMarkdown, - langfuseModel, - pathJoin, - toErrorWithMessage, -} from "./utils"; function resolveLanguageModel( llm: TextGenerateActionContent["llm"], diff --git a/app/(playground)/p/[agentId]/canary/components/editor.tsx b/app/(playground)/p/[agentId]/canary/components/editor.tsx index 03299f9d..b4188bfb 100644 --- a/app/(playground)/p/[agentId]/canary/components/editor.tsx +++ b/app/(playground)/p/[agentId]/canary/components/editor.tsx @@ -17,8 +17,8 @@ import { useMousePosition } from "../contexts/mouse-position"; import { usePropertiesPanel } from "../contexts/properties-panel"; import { useToast } from "../contexts/toast"; import { useToolbar } from "../contexts/toolbar"; +import { createNodeId, isTextGeneration } from "../lib/utils"; import type { NodeId, Tool } from "../types"; -import { createNodeId, isTextGeneration } from "../utils"; import { Edge } from "./edge"; import { Header } from "./header"; import { KeyboardShortcut } from "./keyboard-shortcut"; diff --git a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx index d0e63c7b..192b5fff 100644 --- a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx @@ -46,6 +46,20 @@ import { } from "../contexts/graph"; import { usePropertiesPanel } from "../contexts/properties-panel"; import { useToast } from "../contexts/toast"; +import { + createArtifactId, + createConnectionId, + createFileId, + createNodeHandleId, + createNodeId, + formatTimestamp, + isFile, + isFiles, + isText, + isTextGeneration, + pathJoin, + toErrorWithMessage, +} from "../lib/utils"; import { textGenerationPrompt } from "../prompts"; import type { FileContent, @@ -60,20 +74,6 @@ import type { TextContent, TextGenerateActionContent, } from "../types"; -import { - createArtifactId, - createConnectionId, - createFileId, - createNodeHandleId, - createNodeId, - formatTimestamp, - isFile, - isFiles, - isText, - isTextGeneration, - pathJoin, - toErrorWithMessage, -} from "../utils"; import { Block } from "./block"; import ClipboardButton from "./clipboard-button"; import { ContentTypeIcon } from "./content-type-icon"; diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index 438930f0..aa605971 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -2,8 +2,8 @@ import { type StreamableValue, readStreamableValue } from "ai/rsc"; import { type ReactNode, createContext, useCallback, useContext } from "react"; +import { createArtifactId, toErrorWithMessage } from "../lib/utils"; import type { ArtifactId, NodeId, TextArtifactObject } from "../types"; -import { createArtifactId, toErrorWithMessage } from "../utils"; import { useGraph } from "./graph"; import { usePropertiesPanel } from "./properties-panel"; import { useToast } from "./toast"; diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/graph.ts index e8570bf3..9fd91e85 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/graph.ts @@ -1,3 +1,4 @@ +import { createFlowId, createJobId, createStepId } from "./lib/utils"; import type { Connection, ConnectionId, @@ -9,7 +10,6 @@ import type { NodeId, Step, } from "./types"; -import { createFlowId, createJobId, createStepId } from "./utils"; export function deriveFlows(graph: Graph): Flow[] { const processedNodes = new Set(); diff --git a/app/(playground)/p/[agentId]/canary/utils.test.ts b/app/(playground)/p/[agentId]/canary/lib/utils.test.ts similarity index 100% rename from app/(playground)/p/[agentId]/canary/utils.test.ts rename to app/(playground)/p/[agentId]/canary/lib/utils.test.ts diff --git a/app/(playground)/p/[agentId]/canary/utils.ts b/app/(playground)/p/[agentId]/canary/lib/utils.ts similarity index 99% rename from app/(playground)/p/[agentId]/canary/utils.ts rename to app/(playground)/p/[agentId]/canary/lib/utils.ts index 7cddbc37..f8d6c008 100644 --- a/app/(playground)/p/[agentId]/canary/utils.ts +++ b/app/(playground)/p/[agentId]/canary/lib/utils.ts @@ -1,5 +1,5 @@ import { createId } from "@paralleldrive/cuid2"; -import { vercelBlobGraphFolder } from "./constants"; +import { vercelBlobGraphFolder } from "../constants"; import type { ArtifactId, ConnectionId, @@ -18,7 +18,7 @@ import type { Text, TextGenerateActionContent, TextGeneration, -} from "./types"; +} from "../types"; export function createNodeId(): NodeId { return `nd_${createId()}`; diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index 96958ce5..570136bb 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -15,8 +15,8 @@ import { PropertiesPanelProvider } from "./contexts/properties-panel"; import { ToastProvider } from "./contexts/toast"; import { ToolbarContextProvider } from "./contexts/toolbar"; import { isLatestVersion, migrateGraph } from "./graph"; +import { buildGraphFolderPath } from "./lib/utils"; import type { AgentId, ArtifactId, Graph, NodeId } from "./types"; -import { buildGraphFolderPath } from "./utils"; // Extend the max duration of the server actions from this page to 5 minutes // https://vercel.com/docs/functions/runtimes#max-duration From 4d0618e469e2220c8ec7a2c143d7d1f5c5149afd Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Wed, 11 Dec 2024 23:51:14 +0900 Subject: [PATCH 10/28] refactor(canary): Organize text generation prompts - Move text generation prompts from individual component file to a centralized `lib/prompts` directory for improved organization and clarity. This refactoring facilitates easier access and maintenance of prompt-related functions. - Update import statements in affected files to reference the new location of the prompts. No breaking changes introduced with this update. --- app/(playground)/p/[agentId]/canary/actions.ts | 2 +- .../p/[agentId]/canary/components/properties-panel.tsx | 2 +- app/(playground)/p/[agentId]/canary/{ => lib}/prompts.test.ts | 0 app/(playground)/p/[agentId]/canary/{ => lib}/prompts.ts | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename app/(playground)/p/[agentId]/canary/{ => lib}/prompts.test.ts (100%) rename app/(playground)/p/[agentId]/canary/{ => lib}/prompts.ts (100%) diff --git a/app/(playground)/p/[agentId]/canary/actions.ts b/app/(playground)/p/[agentId]/canary/actions.ts index b68fdc90..80f4f4a6 100644 --- a/app/(playground)/p/[agentId]/canary/actions.ts +++ b/app/(playground)/p/[agentId]/canary/actions.ts @@ -15,6 +15,7 @@ import { UnstructuredClient } from "unstructured-client"; import { Strategy } from "unstructured-client/sdk/models/shared"; import * as v from "valibot"; import { vercelBlobFileFolder, vercelBlobGraphFolder } from "./constants"; +import { textGenerationPrompt } from "./lib/prompts"; import { buildGraphPath, elementsToMarkdown, @@ -22,7 +23,6 @@ import { pathJoin, toErrorWithMessage, } from "./lib/utils"; -import { textGenerationPrompt } from "./prompts"; import type { AgentId, ArtifactId, diff --git a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx index 192b5fff..df4e861e 100644 --- a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx @@ -46,6 +46,7 @@ import { } from "../contexts/graph"; import { usePropertiesPanel } from "../contexts/properties-panel"; import { useToast } from "../contexts/toast"; +import { textGenerationPrompt } from "../lib/prompts"; import { createArtifactId, createConnectionId, @@ -60,7 +61,6 @@ import { pathJoin, toErrorWithMessage, } from "../lib/utils"; -import { textGenerationPrompt } from "../prompts"; import type { FileContent, FileData, diff --git a/app/(playground)/p/[agentId]/canary/prompts.test.ts b/app/(playground)/p/[agentId]/canary/lib/prompts.test.ts similarity index 100% rename from app/(playground)/p/[agentId]/canary/prompts.test.ts rename to app/(playground)/p/[agentId]/canary/lib/prompts.test.ts diff --git a/app/(playground)/p/[agentId]/canary/prompts.ts b/app/(playground)/p/[agentId]/canary/lib/prompts.ts similarity index 100% rename from app/(playground)/p/[agentId]/canary/prompts.ts rename to app/(playground)/p/[agentId]/canary/lib/prompts.ts From 1803f51d2cbc219a00aaf5c2030ada099dfb049a Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Wed, 11 Dec 2024 23:55:22 +0900 Subject: [PATCH 11/28] refactor(graph): Move graph logic to lib directory - Refactor the project structure by moving graph-related logic from the `contexts` and `component` directories to a dedicated `lib` directory. This is intended to enhance the organization and clarity of the codebase. - Update import statements across relevant files to use the new locations for the graph functions and types. No breaking changes are introduced in this refactor. --- app/(playground)/p/[agentId]/canary/contexts/graph.tsx | 2 +- app/(playground)/p/[agentId]/canary/{ => lib}/graph.test.ts | 2 +- app/(playground)/p/[agentId]/canary/{ => lib}/graph.ts | 4 ++-- app/(playground)/p/[agentId]/canary/page.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename app/(playground)/p/[agentId]/canary/{ => lib}/graph.test.ts (99%) rename app/(playground)/p/[agentId]/canary/{ => lib}/graph.ts (98%) diff --git a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx index bfa75c8a..1555a4ad 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx @@ -9,7 +9,7 @@ import { useRef, useState, } from "react"; -import { deriveFlows } from "../graph"; +import { deriveFlows } from "../lib/graph"; import type { Artifact, Connection, diff --git a/app/(playground)/p/[agentId]/canary/graph.test.ts b/app/(playground)/p/[agentId]/canary/lib/graph.test.ts similarity index 99% rename from app/(playground)/p/[agentId]/canary/graph.test.ts rename to app/(playground)/p/[agentId]/canary/lib/graph.test.ts index 08dbb8f5..89445662 100644 --- a/app/(playground)/p/[agentId]/canary/graph.test.ts +++ b/app/(playground)/p/[agentId]/canary/lib/graph.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; +import type { Graph } from "../types"; import { deriveFlows, isLatestVersion, migrateGraph } from "./graph"; -import type { Graph } from "./types"; // graph is the following structure // ┌────────────────────────┐ ┌───────────────────┐ diff --git a/app/(playground)/p/[agentId]/canary/graph.ts b/app/(playground)/p/[agentId]/canary/lib/graph.ts similarity index 98% rename from app/(playground)/p/[agentId]/canary/graph.ts rename to app/(playground)/p/[agentId]/canary/lib/graph.ts index 9fd91e85..b7569ac5 100644 --- a/app/(playground)/p/[agentId]/canary/graph.ts +++ b/app/(playground)/p/[agentId]/canary/lib/graph.ts @@ -1,4 +1,3 @@ -import { createFlowId, createJobId, createStepId } from "./lib/utils"; import type { Connection, ConnectionId, @@ -9,7 +8,8 @@ import type { Node, NodeId, Step, -} from "./types"; +} from "../types"; +import { createFlowId, createJobId, createStepId } from "./utils"; export function deriveFlows(graph: Graph): Flow[] { const processedNodes = new Set(); diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index 570136bb..0ac5f8ee 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -14,7 +14,7 @@ import { MousePositionProvider } from "./contexts/mouse-position"; import { PropertiesPanelProvider } from "./contexts/properties-panel"; import { ToastProvider } from "./contexts/toast"; import { ToolbarContextProvider } from "./contexts/toolbar"; -import { isLatestVersion, migrateGraph } from "./graph"; +import { isLatestVersion, migrateGraph } from "./lib/graph"; import { buildGraphFolderPath } from "./lib/utils"; import type { AgentId, ArtifactId, Graph, NodeId } from "./types"; From 1a02685d3d322115175b2aa0fb1ac4c23c0c3ee2 Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Thu, 12 Dec 2024 00:11:49 +0900 Subject: [PATCH 12/28] feat(header): Add custom button component for header action - Introduce a custom `Button` component in the `ui` directory to standardize button styles across the application. The new button features consistent padding, border-radius, and a box shadow for visual depth. - Update the `Header` component to utilize the new `Button`, replacing the previous static layout with a more interactive button labeled "Run", which now includes a Sparkles icon. This change enhances the user interface and ensures that button styles across different components are consistently applied. --- .../p/[agentId]/canary/components/header.tsx | 11 ++++++---- .../[agentId]/canary/components/ui/button.tsx | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 app/(playground)/p/[agentId]/canary/components/ui/button.tsx diff --git a/app/(playground)/p/[agentId]/canary/components/header.tsx b/app/(playground)/p/[agentId]/canary/components/header.tsx index c6f3fd27..68a9b1bb 100644 --- a/app/(playground)/p/[agentId]/canary/components/header.tsx +++ b/app/(playground)/p/[agentId]/canary/components/header.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import type { ReactNode } from "react"; // import { RunButton } from "./flow/components/run-button"; import { SparklesIcon } from "./icons/sparkles"; +import { Button } from "./ui/button"; function SelectionIndicator() { return ( @@ -75,10 +76,12 @@ export function Header() { edit view
-
- {/*
- -
*/} +
+ +
); } diff --git a/app/(playground)/p/[agentId]/canary/components/ui/button.tsx b/app/(playground)/p/[agentId]/canary/components/ui/button.tsx new file mode 100644 index 00000000..cc4b3952 --- /dev/null +++ b/app/(playground)/p/[agentId]/canary/components/ui/button.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx/lite"; +import type { ButtonHTMLAttributes, DetailedHTMLProps } from "react"; + +export function Button({ + className, + ...props +}: DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +>) { + return ( +
- edit - view + edit + view
diff --git a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx index df4e861e..cf80c679 100644 --- a/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx +++ b/app/(playground)/p/[agentId]/canary/components/properties-panel.tsx @@ -288,7 +288,7 @@ export function PropertiesPanel() { const { graph, dispatch, flush } = useGraph(); const selectedNode = useSelectedNode(); const { open, setOpen, tab, setTab } = usePropertiesPanel(); - const execute = useExecution(); + const { execute } = useExecution(); return (
+ {icon} +

{title}

+

+ {description} +

+ {children} +
+ ); +} diff --git a/app/(playground)/p/[agentId]/canary/components/viewer.tsx b/app/(playground)/p/[agentId]/canary/components/viewer.tsx index b176ab91..cae82de8 100644 --- a/app/(playground)/p/[agentId]/canary/components/viewer.tsx +++ b/app/(playground)/p/[agentId]/canary/components/viewer.tsx @@ -1,9 +1,189 @@ "use client"; +import * as Tabs from "@radix-ui/react-tabs"; +import { type DetailedHTMLProps, useMemo } from "react"; +import { SpinnerIcon } from "../../beta-proto/components/icons/spinner"; +import { WilliIcon } from "../../beta-proto/components/icons/willi"; +import { useExecution } from "../contexts/execution"; +import { useGraph } from "../contexts/graph"; +import type { Execution, Node, StepExecution } from "../types"; import bg from "./bg.png"; +import { ContentTypeIcon } from "./content-type-icon"; import { Header } from "./header"; +import { EmptyState } from "./ui/empty-state"; + +interface StepExecutionButtonProps + extends DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + > { + stepExecution: StepExecution; + node: Node; +} +function StepExecutionButton({ + stepExecution, + node, + ...props +}: StepExecutionButtonProps) { + return ( + + ); +} + +function ExecutionViewer({ + execution: tmpExecution, +}: { execution: Execution }) { + const { graph } = useGraph(); + const execution = useMemo( + () => ({ + ...tmpExecution, + jobExecutions: tmpExecution.jobExecutions.map((jobExecution) => ({ + ...jobExecution, + stepExecutions: jobExecution.stepExecutions + .map((stepExecution) => { + const node = graph.nodes.find( + (node) => node.id === stepExecution.nodeId, + ); + if (node === undefined) { + return null; + } + return { + ...stepExecution, + node, + }; + }) + .filter((stepExecution) => stepExecution !== null), + })), + }), + [tmpExecution, graph.nodes], + ); + + return ( + +
+ + {execution.jobExecutions.map((jobExecution, index) => ( +
+

+ Step {index + 1} +

+
+ {jobExecution.stepExecutions.map((stepExecution) => ( + + + + ))} +
+
+ ))} +
+
+
+ {/* {state.flow.jobs.flatMap((job) => + job.steps + .filter( + (step) => + step.status === stepStatuses.streaming || + step.status === stepStatuses.completed, + ) + .map((step) => ( + + {step.output.object === "artifact.text" ? ( + + ) : ( +
+ + + + + + + + + + + + + + + {step.output.scrapingTasks.map((scrapingTask) => ( + + + + + + ))} + +
StatusContentRelevance
+ {scrapingTask.state === "completed" ? ( + + ) : scrapingTask.state === "failed" ? ( + + ) : ( + "" + )} + +

+ {scrapingTask.title} +

+

+ {scrapingTask.url} +

+
+ {Math.min( + 0.99, + Number.parseFloat( + scrapingTask.relevance.toFixed(2), + ), + )} +
+
+ )} +
+ )), + )} */} +
+
+ ); +} export function Viewer() { + const { execution } = useExecution(); return (
+
+ {execution === null ? ( + + } + title="This has not yet been executed" + description="You have not yet + executed the node. Let's execute entire thing and create the final + output." + /> + ) : ( + + )} +
); } diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index aa605971..93caf125 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -1,15 +1,36 @@ "use client"; import { type StreamableValue, readStreamableValue } from "ai/rsc"; -import { type ReactNode, createContext, useCallback, useContext } from "react"; -import { createArtifactId, toErrorWithMessage } from "../lib/utils"; -import type { ArtifactId, NodeId, TextArtifactObject } from "../types"; +import { + type ReactNode, + createContext, + useCallback, + useContext, + useState, +} from "react"; +import { + createArtifactId, + createExecutionId, + createJobExecutionId, + createStepExecutionId, + toErrorWithMessage, +} from "../lib/utils"; +import type { + ArtifactId, + Execution, + FlowId, + NodeId, + TextArtifactObject, +} from "../types"; import { useGraph } from "./graph"; +import { usePlaygroundMode } from "./playground-mode"; import { usePropertiesPanel } from "./properties-panel"; import { useToast } from "./toast"; interface ExecutionContextType { + execution: Execution | null; execute: (nodeId: NodeId) => Promise; + executeFlow: (flowId: FlowId) => Promise; } const ExecutionContext = createContext( @@ -28,9 +49,12 @@ export function ExecutionProvider({ children, executeAction, }: ExecutionProviderProps) { - const { dispatch, flush } = useGraph(); + const { dispatch, flush, graph } = useGraph(); const { setTab } = usePropertiesPanel(); const { addToast } = useToast(); + const { setPlaygroundMode } = usePlaygroundMode(); + const [execution, setExecution] = useState(null); + const execute = useCallback( async (nodeId: NodeId) => { const artifactId = createArtifactId(); @@ -119,8 +143,33 @@ export function ExecutionProvider({ }, [executeAction, dispatch, flush, setTab, addToast], ); + + const executeFlow = useCallback( + async (flowId: FlowId) => { + const flow = graph.flows.find((flow) => flow.id === flowId); + if (flow === undefined) { + throw new Error("Flow not found"); + } + setPlaygroundMode("viewer"); + setExecution({ + id: createExecutionId(), + status: "pending", + flowId, + jobExecutions: flow.jobs.map((job) => ({ + id: createJobExecutionId(), + status: "pending", + stepExecutions: job.steps.map((step) => ({ + id: createStepExecutionId(), + nodeId: step.nodeId, + status: "pending", + })), + })), + }); + }, + [setPlaygroundMode, graph.flows], + ); return ( - + {children} ); @@ -131,5 +180,5 @@ export function useExecution() { if (!context) { throw new Error("useExecution must be used within an ExecutionProvider"); } - return context.execute; + return context; } diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts new file mode 100644 index 00000000..5730d169 --- /dev/null +++ b/app/(playground)/p/[agentId]/canary/lib/execution.ts @@ -0,0 +1,23 @@ +import type { Execution, Flow, Graph, Job, JobExecution } from "../types"; +import { createExecutionId, createJobExecutionId } from "./utils"; + +function createJobExecutionFromJob(job: Job): JobExecution { + return { + id: createJobExecutionId(), + status: "pending", + stepExecutions: job.steps.map((step) => ({ + id: `stex_${step.id}`, + nodeId: step.nodeId, + status: "pending", + })), + }; +} + +export function createExecutionFromFlow(flow: Flow, graph: Graph): Execution { + return { + id: createExecutionId(), + status: "pending", + flowId: flow.id, + jobExecutions: flow.jobs.map(createJobExecutionFromJob), + }; +} diff --git a/app/(playground)/p/[agentId]/canary/lib/utils.ts b/app/(playground)/p/[agentId]/canary/lib/utils.ts index f8d6c008..4c6e9e55 100644 --- a/app/(playground)/p/[agentId]/canary/lib/utils.ts +++ b/app/(playground)/p/[agentId]/canary/lib/utils.ts @@ -3,17 +3,20 @@ import { vercelBlobGraphFolder } from "../constants"; import type { ArtifactId, ConnectionId, + ExecutionId, File, FileId, Files, FlowId, Graph, GraphId, + JobExecutionId, JobId, LatestGraphVersion, Node, NodeHandleId, NodeId, + StepExecutionId, StepId, Text, TextGenerateActionContent, @@ -55,6 +58,18 @@ export function createStepId(): StepId { return `stp_${createId()}`; } +export function createStepExecutionId(): StepExecutionId { + return `stex_${createId()}`; +} + +export function createJobExecutionId(): JobExecutionId { + return `jbex_${createId()}`; +} + +export function createExecutionId(): ExecutionId { + return `exct_${createId()}`; +} + export function isTextGeneration(node: Node): node is TextGeneration { return node.content.type === "textGeneration"; } diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index ae164260..829caeb4 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -226,3 +226,73 @@ export interface Flow { } export type AgentId = `agnt_${string}`; + +export type StepExecutionId = `stex_${string}`; +interface StepExecutionBase { + id: StepExecutionId; + nodeId: NodeId; + status: string; +} +interface PendingStepExecution extends StepExecutionBase { + status: "pending"; +} + +interface RunningStepExecution extends StepExecutionBase { + status: "running"; + runStartedAt: number; +} + +interface CompletedStepExecution extends StepExecutionBase { + status: "completed"; + runStartedAt: number; + durationMs: number; +} +export type StepExecution = + | PendingStepExecution + | RunningStepExecution + | CompletedStepExecution; + +export type JobExecutionId = `jbex_${string}`; +interface JobExecutionBase { + id: JobExecutionId; + stepExecutions: StepExecution[]; + status: string; +} +interface PendingJobExecution extends JobExecutionBase { + status: "pending"; +} +interface RunningJobExecution extends JobExecutionBase { + status: "running"; + runStartedAt: number; +} +interface CompletedJobExecution extends JobExecutionBase { + status: "completed"; + runStartedAt: number; + durationMs: number; +} +export type JobExecution = + | PendingJobExecution + | RunningJobExecution + | CompletedJobExecution; +export type ExecutionId = `exct_${string}`; +interface ExecutionBase { + id: ExecutionId; + flowId?: FlowId; + jobExecutions: JobExecution[]; +} +interface PendingExecution extends ExecutionBase { + status: "pending"; +} +interface RunningExecution extends ExecutionBase { + status: "running"; + runStartedAt: number; +} +interface CompletedExecution extends ExecutionBase { + status: "completed"; + runStartedAt: number; + durationMs: number; +} +export type Execution = + | PendingExecution + | RunningExecution + | CompletedExecution; From 628dbcf0c8cb527c8a2bcc6407a8ac15bc165147 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 14:21:22 +0900 Subject: [PATCH 16/28] refactor: Remove unused execution utility module This commit removes the execution.ts module that contained unused utility functions for creating execution objects from flows and jobs. This code was previously used for flow execution tracking but has been superseded by the new execution engine implementation. This change helps reduce codebase complexity by removing dead code. The functionality has been reimplemented in the new execution system. --- .../p/[agentId]/canary/lib/execution.ts | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 app/(playground)/p/[agentId]/canary/lib/execution.ts diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts deleted file mode 100644 index 5730d169..00000000 --- a/app/(playground)/p/[agentId]/canary/lib/execution.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Execution, Flow, Graph, Job, JobExecution } from "../types"; -import { createExecutionId, createJobExecutionId } from "./utils"; - -function createJobExecutionFromJob(job: Job): JobExecution { - return { - id: createJobExecutionId(), - status: "pending", - stepExecutions: job.steps.map((step) => ({ - id: `stex_${step.id}`, - nodeId: step.nodeId, - status: "pending", - })), - }; -} - -export function createExecutionFromFlow(flow: Flow, graph: Graph): Execution { - return { - id: createExecutionId(), - status: "pending", - flowId: flow.id, - jobExecutions: flow.jobs.map(createJobExecutionFromJob), - }; -} From 2e6d37a5d6881fc898f6a2ae5adb646221f77f22 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 14:21:55 +0900 Subject: [PATCH 17/28] feat(viewer): Add execution step status display in tabs Add initial implementation of execution step status display in the viewer component. Each step execution is now shown in its own tab panel with basic status information. The change introduces a first pass at showing execution progress, displaying step IDs and their current status. This lays the groundwork for more detailed execution monitoring UI to be added later. Note: Currently shows minimal info - will be enhanced with more detailed execution data and styling in follow-up PRs. --- app/(playground)/p/[agentId]/canary/components/viewer.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/(playground)/p/[agentId]/canary/components/viewer.tsx b/app/(playground)/p/[agentId]/canary/components/viewer.tsx index cae82de8..16b40c30 100644 --- a/app/(playground)/p/[agentId]/canary/components/viewer.tsx +++ b/app/(playground)/p/[agentId]/canary/components/viewer.tsx @@ -111,6 +111,13 @@ function ExecutionViewer({
+ {execution.jobExecutions.flatMap((jobExecution) => + jobExecution.stepExecutions.map((stepExecution) => ( + + {stepExecution.id},{stepExecution.status} + + )), + )} {/* {state.flow.jobs.flatMap((job) => job.steps .filter( From 1ff303be684d53259e7d4df03d57b8fcd88be103 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 16:44:35 +0900 Subject: [PATCH 18/28] feat(execution): Implement step-by-step flow execution engine Add comprehensive flow execution system with status tracking and step orchestration. This implementation includes: - New server-side execution logic for processing steps - Status tracking for jobs and individual steps - Integration with language model providers (OpenAI, Anthropic, Google) - Langfuse tracing for monitoring execution - Support for various node types (text, file, text generation) The execution engine now properly handles step sequencing, artifact resolution, and provides real-time status updates. Each step execution is tracked with timing data and proper state management. BREAKING: Execution types updated to include additional required fields (stepId, jobId) for better tracing and state management. --- .../p/[agentId]/canary/contexts/execution.tsx | 58 ++- .../p/[agentId]/canary/lib/execution.ts | 350 ++++++++++++++++++ app/(playground)/p/[agentId]/canary/types.ts | 2 + 3 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 app/(playground)/p/[agentId]/canary/lib/execution.ts diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index 93caf125..841c310a 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -18,8 +18,10 @@ import { import type { ArtifactId, Execution, + ExecutionId, FlowId, NodeId, + StepId, TextArtifactObject, } from "../types"; import { useGraph } from "./graph"; @@ -43,11 +45,17 @@ interface ExecutionProviderProps { artifactId: ArtifactId, nodeId: NodeId, ) => Promise>; + executeStepAction: ( + flowId: FlowId, + executionId: ExecutionId, + stepId: StepId, + ) => Promise>; } export function ExecutionProvider({ children, executeAction, + executeStepAction, }: ExecutionProviderProps) { const { dispatch, flush, graph } = useGraph(); const { setTab } = usePropertiesPanel(); @@ -151,22 +159,64 @@ export function ExecutionProvider({ throw new Error("Flow not found"); } setPlaygroundMode("viewer"); - setExecution({ + let execution: Execution = { id: createExecutionId(), - status: "pending", + status: "running", + runStartedAt: Date.now(), flowId, jobExecutions: flow.jobs.map((job) => ({ id: createJobExecutionId(), + jobId: job.id, status: "pending", stepExecutions: job.steps.map((step) => ({ id: createStepExecutionId(), + stepId: step.id, nodeId: step.nodeId, status: "pending", })), })), - }); + }; + setExecution(execution); + for (const currentJobExecution of execution.jobExecutions) { + const jobRunStartedAt = Date.now(); + execution = { + ...execution, + jobExecutions: execution.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + status: "running", + runStartedAt: jobRunStartedAt, + }; + }), + }; + setExecution(execution); + let durationMs = 0; + await Promise.all( + currentJobExecution.stepExecutions.map(async (stepExecution) => { + executeStepAction(flowId, execution.id, stepExecution.stepId); + durationMs += Date.now() - jobRunStartedAt; + }), + ); + execution = { + ...execution, + jobExecutions: execution.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + status: "completed", + runStartedAt: jobRunStartedAt, + durationMs, + }; + }), + }; + } }, - [setPlaygroundMode, graph.flows], + [setPlaygroundMode, graph.flows, executeStepAction], ); return ( diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts new file mode 100644 index 00000000..95861257 --- /dev/null +++ b/app/(playground)/p/[agentId]/canary/lib/execution.ts @@ -0,0 +1,350 @@ +"use server"; + +import { db } from "@/drizzle"; +import { anthropic } from "@ai-sdk/anthropic"; +import { google } from "@ai-sdk/google"; +import { openai } from "@ai-sdk/openai"; +import { toJsonSchema } from "@valibot/to-json-schema"; +import { type LanguageModelV1, jsonSchema, streamObject } from "ai"; +import { createStreamableValue } from "ai/rsc"; +import { MockLanguageModelV1, simulateReadableStream } from "ai/test"; +import HandleBars from "handlebars"; +import Langfuse from "langfuse"; +import * as v from "valibot"; +import type { + AgentId, + Artifact, + ExecutionId, + FlowId, + Graph, + Node, + NodeHandle, + NodeHandleId, + NodeId, + StepId, + TextArtifactObject, + TextGenerateActionContent, +} from "../types"; +import { textGenerationPrompt } from "./prompts"; +import { langfuseModel } from "./utils"; + +function resolveLanguageModel( + llm: TextGenerateActionContent["llm"], +): LanguageModelV1 { + const [provider, model] = llm.split(":"); + if (provider === "openai") { + return openai(model); + } + if (provider === "anthropic") { + return anthropic(model); + } + if (provider === "google") { + return google(model); + } + if (provider === "dev") { + return new MockLanguageModelV1({ + defaultObjectGenerationMode: "json", + doStream: async () => ({ + stream: simulateReadableStream({ + values: [{ type: "error", error: "a" }], + }), + rawCall: { rawPrompt: null, rawSettings: {} }, + }), + }); + } + throw new Error("Unsupported model provider"); +} + +const artifactSchema = v.object({ + plan: v.pipe( + v.string(), + v.description( + "How you think about the content of the artefact (purpose, structure, essentials) and how you intend to output it", + ), + ), + title: v.pipe(v.string(), v.description("The title of the artefact")), + content: v.pipe( + v.string(), + v.description("The content of the artefact formatted markdown."), + ), + description: v.pipe( + v.string(), + v.description( + "Explanation of the Artifact and what the intention was in creating this Artifact. Add any suggestions for making it even better.", + ), + ), +}); + +type ArtifactResolver = (artifactCreatorNodeId: NodeId) => Artifact | null; +type NodeResolver = (nodeHandleId: NodeHandleId) => Node | null; +interface SourceResolver { + nodeResolver: NodeResolver; + artifactResolver: ArtifactResolver; +} + +interface ExecutionSourceBase { + type: string; + nodeId: NodeId; +} + +interface TextSource extends ExecutionSourceBase { + type: "text"; + content: string; +} +interface FileSource extends ExecutionSourceBase { + type: "file"; + title: string; + content: string; +} +interface TextGenerationSource extends ExecutionSourceBase { + type: "textGeneration"; + title: string; + content: string; +} + +type ExecutionSource = TextSource | TextGenerationSource | FileSource; +async function resolveSources(sources: NodeHandle[], resolver: SourceResolver) { + return Promise.all( + sources.map(async (source) => { + const node = resolver.nodeResolver(source.id); + switch (node?.content.type) { + case "text": + return { + type: "text", + content: node.content.text, + nodeId: node.id, + } satisfies ExecutionSource; + case "file": { + if (node.content.data == null) { + throw new Error("File not found"); + } + if (node.content.data.status === "uploading") { + /** @todo Let user know file is uploading*/ + throw new Error("File is uploading"); + } + if (node.content.data.status === "processing") { + /** @todo Let user know file is processing*/ + throw new Error("File is processing"); + } + if (node.content.data.status === "failed") { + return null; + } + const text = await fetch(node.content.data.textDataUrl).then((res) => + res.text(), + ); + return { + type: "file", + title: node.content.data.name, + content: text, + nodeId: node.id, + } satisfies ExecutionSource; + } + + case "files": { + return await Promise.all( + node.content.data.map(async (file) => { + if (file == null) { + throw new Error("File not found"); + } + if (file.status === "uploading") { + /** @todo Let user know file is uploading*/ + throw new Error("File is uploading"); + } + if (file.status === "processing") { + /** @todo Let user know file is processing*/ + throw new Error("File is processing"); + } + if (file.status === "failed") { + return null; + } + const text = await fetch(file.textDataUrl).then((res) => + res.text(), + ); + return { + type: "file", + title: file.name, + content: text, + nodeId: node.id, + } satisfies ExecutionSource; + }), + ); + } + case "textGeneration": { + const generatedArtifact = resolver.artifactResolver(node.id); + if ( + generatedArtifact === null || + generatedArtifact.type !== "generatedArtifact" + ) { + return null; + } + return { + type: "textGeneration", + title: generatedArtifact.object.title, + content: generatedArtifact.object.content, + nodeId: node.id, + } satisfies ExecutionSource; + } + default: + return null; + } + }), + ).then((sources) => sources.filter((source) => source !== null).flat()); +} + +interface RequirementResolver { + nodeResolver: NodeResolver; + artifactResolver: ArtifactResolver; +} + +function resolveRequirement( + requirement: NodeHandle | null, + resolver: RequirementResolver, +) { + if (requirement === null) { + return null; + } + const node = resolver.nodeResolver(requirement.id); + switch (node?.content.type) { + case "text": + return node.content.text; + case "textGeneration": { + const generatedArtifact = resolver.artifactResolver(node.id); + if ( + generatedArtifact === null || + generatedArtifact.type === "generatedArtifact" + ) { + return null; + } + return generatedArtifact.object.content; + } + default: + return null; + } +} + +export async function executeStep( + agentId: AgentId, + flowId: FlowId, + executionId: ExecutionId, + stepId: StepId, +) { + const lf = new Langfuse(); + const trace = lf.trace({ + sessionId: executionId, + }); + const agent = await db.query.agents.findFirst({ + where: (agents, { eq }) => eq(agents.id, agentId), + }); + if (agent === undefined || agent.graphUrl === null) { + throw new Error(`Agent with id ${agentId} not found`); + } + + const graph = await fetch(agent.graphUrl).then( + (res) => res.json() as unknown as Graph, + ); + const flow = graph.flows.find((flow) => flow.id === flowId); + if (flow === undefined) { + throw new Error(`Flow with id ${flowId} not found`); + } + const step = flow.jobs + .flatMap((job) => job.steps) + .find((step) => step.id === stepId); + if (step === undefined) { + throw new Error(`Step with id ${stepId} not found`); + } + const node = graph.nodes.find((node) => node.id === step.nodeId); + if (node === undefined) { + throw new Error("Node not found"); + } + + function nodeResolver(nodeHandleId: NodeHandleId) { + const connection = graph.connections.find( + (connection) => connection.targetNodeHandleId === nodeHandleId, + ); + const node = graph.nodes.find( + (node) => node.id === connection?.sourceNodeId, + ); + if (node === undefined) { + return null; + } + return node; + } + function artifactResolver(artifactCreatorNodeId: NodeId) { + const generatedArtifact = graph.artifacts.find( + (artifact) => artifact.creatorNodeId === artifactCreatorNodeId, + ); + if ( + generatedArtifact === undefined || + generatedArtifact.type === "generatedArtifact" + ) { + return null; + } + return generatedArtifact; + } + // The main switch statement handles the different types of nodes + switch (node.content.type) { + case "textGeneration": { + const actionSources = await resolveSources(node.content.sources, { + nodeResolver, + artifactResolver, + }); + const requirement = resolveRequirement(node.content.requirement ?? null, { + nodeResolver, + artifactResolver, + }); + const model = resolveLanguageModel(node.content.llm); + const promptTemplate = HandleBars.compile( + node.content.system ?? textGenerationPrompt, + ); + const prompt = promptTemplate({ + instruction: node.content.instruction, + requirement, + sources: actionSources, + }); + const topP = node.content.topP; + const temperature = node.content.temperature; + const stream = createStreamableValue(); + + const generationTracer = trace.generation({ + name: "generate-text", + input: prompt, + model: langfuseModel(node.content.llm), + modelParameters: { + topP: node.content.topP, + temperature: node.content.temperature, + }, + }); + (async () => { + const { partialObjectStream, object } = await streamObject({ + model, + prompt, + schema: jsonSchema>( + toJsonSchema(artifactSchema), + ), + topP, + temperature, + }); + + for await (const partialObject of partialObjectStream) { + stream.update({ + type: "text", + title: partialObject.title ?? "", + content: partialObject.content ?? "", + messages: { + plan: partialObject.plan ?? "", + description: partialObject.description ?? "", + }, + }); + } + const result = await object; + generationTracer.end({ output: result }); + stream.done(); + })().catch((error) => { + stream.error(error); + }); + return stream.value; + } + default: + throw new Error("Invalid node type"); + } +} diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index 829caeb4..dedc1325 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -230,6 +230,7 @@ export type AgentId = `agnt_${string}`; export type StepExecutionId = `stex_${string}`; interface StepExecutionBase { id: StepExecutionId; + stepId: StepId; nodeId: NodeId; status: string; } @@ -255,6 +256,7 @@ export type StepExecution = export type JobExecutionId = `jbex_${string}`; interface JobExecutionBase { id: JobExecutionId; + jobId: JobId; stepExecutions: StepExecution[]; status: string; } From 2b927f4634a087351ec6e55235002ff19cbab18f Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 17:47:22 +0900 Subject: [PATCH 19/28] feat(execution): Add streaming artifacts to flow execution Implement real-time artifact streaming and tracking during flow execution: - Add support for step-by-step artifact generation and updates - Track execution status, timing, and results at step/job/flow levels - Store artifacts as both stream and generated types with metadata - Display execution progress and results in real-time This change enhances the execution context to provide richer feedback and better observability during flow runs. --- .../p/[agentId]/canary/contexts/execution.tsx | 260 ++++++++++++++---- app/(playground)/p/[agentId]/canary/page.tsx | 25 +- app/(playground)/p/[agentId]/canary/types.ts | 3 + 3 files changed, 236 insertions(+), 52 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index 841c310a..c8dd9b80 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -20,6 +20,7 @@ import type { Execution, ExecutionId, FlowId, + JobExecution, NodeId, StepId, TextArtifactObject, @@ -159,62 +160,221 @@ export function ExecutionProvider({ throw new Error("Flow not found"); } setPlaygroundMode("viewer"); - let execution: Execution = { - id: createExecutionId(), - status: "running", - runStartedAt: Date.now(), - flowId, - jobExecutions: flow.jobs.map((job) => ({ - id: createJobExecutionId(), - jobId: job.id, + const executionId = createExecutionId(); + const jobExecutions: JobExecution[] = flow.jobs.map((job) => ({ + id: createJobExecutionId(), + jobId: job.id, + status: "pending", + stepExecutions: job.steps.map((step) => ({ + id: createStepExecutionId(), + stepId: step.id, + nodeId: step.nodeId, status: "pending", - stepExecutions: job.steps.map((step) => ({ - id: createStepExecutionId(), - stepId: step.id, - nodeId: step.nodeId, - status: "pending", - })), })), - }; - setExecution(execution); - for (const currentJobExecution of execution.jobExecutions) { + })); + const flowRunStartedAt = Date.now(); + let flowDurationMs = 0; + setExecution({ + id: executionId, + status: "running", + runStartedAt: flowRunStartedAt, + flowId, + jobExecutions, + artifacts: [], + }); + for (const currentJobExecution of jobExecutions) { const jobRunStartedAt = Date.now(); - execution = { - ...execution, - jobExecutions: execution.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - status: "running", - runStartedAt: jobRunStartedAt, - }; - }), - }; - setExecution(execution); - let durationMs = 0; + setExecution((prev) => { + if (prev === null) { + return null; + } + return { + ...prev, + jobExecutions: prev.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + status: "running", + runStartedAt: jobRunStartedAt, + }; + }), + }; + }); + let jobDurationMs = 0; await Promise.all( - currentJobExecution.stepExecutions.map(async (stepExecution) => { - executeStepAction(flowId, execution.id, stepExecution.stepId); - durationMs += Date.now() - jobRunStartedAt; - }), + currentJobExecution.stepExecutions.map( + async (currentStepExecution) => { + const stepRunStartedAt = Date.now(); + const artifactId = createArtifactId(); + let textArtifactObject: TextArtifactObject = { + type: "text", + title: "", + content: "", + messages: { + plan: "", + description: "", + }, + }; + setExecution((prev) => { + if (prev === null) { + return null; + } + return { + ...prev, + jobExecutions: prev.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + stepExecutions: jobExecution.stepExecutions.map( + (stepExecution) => { + if (stepExecution.id !== currentStepExecution.id) { + return stepExecution; + } + return { + ...stepExecution, + status: "running", + runStartedAt: stepRunStartedAt, + }; + }, + ), + }; + }), + artifacts: + prev.status === "pending" + ? [ + { + id: artifactId, + type: "streamArtifact", + creatorNodeId: currentStepExecution.nodeId, + object: textArtifactObject, + }, + ] + : [ + ...prev.artifacts, + { + id: artifactId, + type: "streamArtifact", + creatorNodeId: currentStepExecution.nodeId, + object: textArtifactObject, + }, + ], + }; + }); + const stream = await executeStepAction( + flowId, + executionId, + currentStepExecution.stepId, + ); + + for await (const streamContent of readStreamableValue(stream)) { + if (streamContent === undefined) { + continue; + } + textArtifactObject = { + ...textArtifactObject, + ...streamContent, + }; + setExecution((prev) => { + if (prev === null || prev.status !== "running") { + return null; + } + return { + ...prev, + artifacts: prev.artifacts.map((artifact) => { + if (artifact.id !== artifactId) { + return artifact; + } + return { + ...artifact, + object: textArtifactObject, + }; + }), + }; + }); + } + const stepDurationMs = Date.now() - stepRunStartedAt; + setExecution((prev) => { + if (prev === null || prev.status !== "running") { + return null; + } + return { + ...prev, + jobExecutions: prev.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + stepExecutions: jobExecution.stepExecutions.map( + (stepExecution) => { + if (stepExecution.id !== currentStepExecution.id) { + return stepExecution; + } + return { + ...stepExecution, + status: "completed", + runStartedAt: stepRunStartedAt, + durationMs: stepDurationMs, + }; + }, + ), + }; + }), + artifacts: prev.artifacts.map((artifact) => { + if (artifact.id !== artifactId) { + return artifact; + } + return { + id: artifactId, + type: "generatedArtifact", + creatorNodeId: currentStepExecution.nodeId, + createdAt: Date.now(), + object: textArtifactObject, + }; + }), + }; + }); + jobDurationMs += stepDurationMs; + }, + ), ); - execution = { - ...execution, - jobExecutions: execution.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - status: "completed", - runStartedAt: jobRunStartedAt, - durationMs, - }; - }), - }; + setExecution((prev) => { + if (prev === null) { + return null; + } + return { + ...prev, + jobExecutions: prev.jobExecutions.map((jobExecution) => { + if (jobExecution.id !== currentJobExecution.id) { + return jobExecution; + } + return { + ...jobExecution, + status: "completed", + runStartedAt: jobRunStartedAt, + durationMs: jobDurationMs, + }; + }), + }; + }); + flowDurationMs += jobDurationMs; } + setExecution((prev) => { + if (prev === null || prev.status !== "running") { + return null; + } + return { + ...prev, + status: "completed", + runStartedAt: flowRunStartedAt, + durationMs: flowDurationMs, + resultArtifact: prev.artifacts[prev.artifacts.length - 1], + }; + }); }, [setPlaygroundMode, graph.flows, executeStepAction], ); diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index 944f0c5f..80781e47 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -15,9 +15,18 @@ import { PlaygroundModeProvider } from "./contexts/playground-mode"; import { PropertiesPanelProvider } from "./contexts/properties-panel"; import { ToastProvider } from "./contexts/toast"; import { ToolbarContextProvider } from "./contexts/toolbar"; +import { executeStep } from "./lib/execution"; import { isLatestVersion, migrateGraph } from "./lib/graph"; import { buildGraphFolderPath } from "./lib/utils"; -import type { AgentId, ArtifactId, Graph, NodeId } from "./types"; +import type { + AgentId, + ArtifactId, + ExecutionId, + FlowId, + Graph, + NodeId, + StepId, +} from "./types"; // Extend the max duration of the server actions from this page to 5 minutes // https://vercel.com/docs/functions/runtimes#max-duration @@ -93,6 +102,15 @@ export default async function Page({ return await action(artifactId, agentId, nodeId); } + async function executeStepAction( + flowId: FlowId, + executionId: ExecutionId, + stepId: StepId, + ) { + "use server"; + return await executeStep(agentId, flowId, executionId, stepId); + } + return ( - + diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index dedc1325..ae7fc35a 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -288,11 +288,14 @@ interface PendingExecution extends ExecutionBase { interface RunningExecution extends ExecutionBase { status: "running"; runStartedAt: number; + artifacts: Artifact[]; } interface CompletedExecution extends ExecutionBase { status: "completed"; runStartedAt: number; durationMs: number; + artifacts: Artifact[]; + resultArtifact: Artifact; } export type Execution = | PendingExecution From 772c367228f009823c747a5eebbfaf0af9d62ac6 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 18:01:07 +0900 Subject: [PATCH 20/28] refactor(execution): Restructure flow execution logic for clarity Improve execution context organization and maintainability: - Extract execution logic into focused helper functions - Create consistent state management patterns - Add type safety improvements - Simplify complex nested execution flows - Standardize artifact handling across execution states The execution logic is now more modular, easier to test, and follows consistent patterns for state updates and error handling. --- .../p/[agentId]/canary/contexts/execution.tsx | 448 ++++++++++-------- app/(playground)/p/[agentId]/canary/types.ts | 3 +- 2 files changed, 239 insertions(+), 212 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index c8dd9b80..f6227ec2 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -19,9 +19,11 @@ import type { ArtifactId, Execution, ExecutionId, + Flow, FlowId, JobExecution, NodeId, + StepExecution, StepId, TextArtifactObject, } from "../types"; @@ -30,6 +32,215 @@ import { usePlaygroundMode } from "./playground-mode"; import { usePropertiesPanel } from "./properties-panel"; import { useToast } from "./toast"; +// Helper functions for execution state management +const createInitialJobExecutions = (flow: Flow): JobExecution[] => { + return flow.jobs.map((job) => ({ + id: createJobExecutionId(), + jobId: job.id, + status: "pending", + stepExecutions: job.steps.map((step) => ({ + id: createStepExecutionId(), + stepId: step.id, + nodeId: step.nodeId, + status: "pending", + })), + })); +}; + +const createInitialExecution = ( + flowId: FlowId, + executionId: ExecutionId, + jobExecutions: JobExecution[], +): Execution => ({ + id: executionId, + status: "running", + runStartedAt: Date.now(), + flowId, + jobExecutions, + artifacts: [], +}); + +const processStreamContent = async ( + stream: StreamableValue, + updateArtifact: (content: TextArtifactObject) => void, +): Promise => { + let textArtifactObject: TextArtifactObject = { + type: "text", + title: "", + content: "", + messages: { plan: "", description: "" }, + }; + + for await (const streamContent of readStreamableValue(stream)) { + if (streamContent === undefined) continue; + textArtifactObject = { ...textArtifactObject, ...streamContent }; + updateArtifact(textArtifactObject); + } + + return textArtifactObject; +}; + +const executeStep = async ( + flowId: FlowId, + executionId: ExecutionId, + stepExecution: StepExecution, + executeStepAction: ExecuteStepAction, + updateExecution: ( + updater: (prev: Execution | null) => Execution | null, + ) => void, +): Promise => { + const stepRunStartedAt = Date.now(); + const artifactId = createArtifactId(); + + // Initialize step execution + updateExecution((prev) => { + if (!prev) return null; + return { + ...prev, + jobExecutions: prev.jobExecutions.map((job) => ({ + ...job, + stepExecutions: job.stepExecutions.map((step) => + step.id === stepExecution.id + ? { ...step, status: "running", runStartedAt: stepRunStartedAt } + : step, + ), + })), + artifacts: [ + ...prev.artifacts, + { + id: artifactId, + type: "streamArtifact", + creatorNodeId: stepExecution.nodeId, + object: { + type: "text", + title: "", + content: "", + messages: { plan: "", description: "" }, + }, + }, + ], + }; + }); + + // Execute step and process stream + const stream = await executeStepAction( + flowId, + executionId, + stepExecution.stepId, + ); + const finalArtifact = await processStreamContent(stream, (content) => { + updateExecution((prev) => { + if (!prev || prev.status !== "running") return null; + return { + ...prev, + artifacts: prev.artifacts.map((artifact) => + artifact.id === artifactId + ? { ...artifact, object: content } + : artifact, + ), + }; + }); + }); + + // Complete step execution + const stepDurationMs = Date.now() - stepRunStartedAt; + updateExecution((prev) => { + if (!prev || prev.status !== "running") return null; + return { + ...prev, + jobExecutions: prev.jobExecutions.map((job) => ({ + ...job, + stepExecutions: job.stepExecutions.map((step) => + step.id === stepExecution.id + ? { + ...step, + status: "completed", + runStartedAt: stepRunStartedAt, + durationMs: stepDurationMs, + } + : step, + ), + })), + artifacts: prev.artifacts.map((artifact) => + artifact.id === artifactId + ? { + id: artifactId, + type: "generatedArtifact", + creatorNodeId: stepExecution.nodeId, + createdAt: Date.now(), + object: finalArtifact, + } + : artifact, + ), + }; + }); + + return stepDurationMs; +}; + +const executeJob = async ( + flowId: FlowId, + executionId: ExecutionId, + jobExecution: JobExecution, + executeStepAction: ExecuteStepAction, + updateExecution: ( + updater: (prev: Execution | null) => Execution | null, + ) => void, +): Promise => { + const jobRunStartedAt = Date.now(); + + // Start job execution + updateExecution((prev) => { + if (!prev) return null; + return { + ...prev, + jobExecutions: prev.jobExecutions.map((job) => + job.id === jobExecution.id + ? { ...job, status: "running", runStartedAt: jobRunStartedAt } + : job, + ), + }; + }); + + // Execute all steps in parallel + const stepDurations = await Promise.all( + jobExecution.stepExecutions.map((step) => + executeStep( + flowId, + executionId, + step, + executeStepAction, + updateExecution, + ), + ), + ); + + const jobDurationMs = stepDurations.reduce( + (sum, duration) => sum + duration, + 0, + ); + + // Complete job execution + updateExecution((prev) => { + if (!prev) return null; + return { + ...prev, + jobExecutions: prev.jobExecutions.map((job) => + job.id === jobExecution.id + ? { + ...job, + status: "completed", + runStartedAt: jobRunStartedAt, + durationMs: jobDurationMs, + } + : job, + ), + }; + }); + + return jobDurationMs; +}; + interface ExecutionContextType { execution: Execution | null; execute: (nodeId: NodeId) => Promise; @@ -40,17 +251,18 @@ const ExecutionContext = createContext( undefined, ); +type ExecuteStepAction = ( + flowId: FlowId, + executionId: ExecutionId, + stepId: StepId, +) => Promise>; interface ExecutionProviderProps { children: ReactNode; executeAction: ( artifactId: ArtifactId, nodeId: NodeId, ) => Promise>; - executeStepAction: ( - flowId: FlowId, - executionId: ExecutionId, - stepId: StepId, - ) => Promise>; + executeStepAction: ExecuteStepAction; } export function ExecutionProvider({ @@ -156,222 +368,38 @@ export function ExecutionProvider({ const executeFlow = useCallback( async (flowId: FlowId) => { const flow = graph.flows.find((flow) => flow.id === flowId); - if (flow === undefined) { - throw new Error("Flow not found"); - } + if (!flow) throw new Error("Flow not found"); + setPlaygroundMode("viewer"); const executionId = createExecutionId(); - const jobExecutions: JobExecution[] = flow.jobs.map((job) => ({ - id: createJobExecutionId(), - jobId: job.id, - status: "pending", - stepExecutions: job.steps.map((step) => ({ - id: createStepExecutionId(), - stepId: step.id, - nodeId: step.nodeId, - status: "pending", - })), - })); + const jobExecutions = createInitialJobExecutions(flow); const flowRunStartedAt = Date.now(); - let flowDurationMs = 0; - setExecution({ - id: executionId, - status: "running", - runStartedAt: flowRunStartedAt, - flowId, - jobExecutions, - artifacts: [], - }); - for (const currentJobExecution of jobExecutions) { - const jobRunStartedAt = Date.now(); - setExecution((prev) => { - if (prev === null) { - return null; - } - return { - ...prev, - jobExecutions: prev.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - status: "running", - runStartedAt: jobRunStartedAt, - }; - }), - }; - }); - let jobDurationMs = 0; - await Promise.all( - currentJobExecution.stepExecutions.map( - async (currentStepExecution) => { - const stepRunStartedAt = Date.now(); - const artifactId = createArtifactId(); - let textArtifactObject: TextArtifactObject = { - type: "text", - title: "", - content: "", - messages: { - plan: "", - description: "", - }, - }; - setExecution((prev) => { - if (prev === null) { - return null; - } - return { - ...prev, - jobExecutions: prev.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - stepExecutions: jobExecution.stepExecutions.map( - (stepExecution) => { - if (stepExecution.id !== currentStepExecution.id) { - return stepExecution; - } - return { - ...stepExecution, - status: "running", - runStartedAt: stepRunStartedAt, - }; - }, - ), - }; - }), - artifacts: - prev.status === "pending" - ? [ - { - id: artifactId, - type: "streamArtifact", - creatorNodeId: currentStepExecution.nodeId, - object: textArtifactObject, - }, - ] - : [ - ...prev.artifacts, - { - id: artifactId, - type: "streamArtifact", - creatorNodeId: currentStepExecution.nodeId, - object: textArtifactObject, - }, - ], - }; - }); - const stream = await executeStepAction( - flowId, - executionId, - currentStepExecution.stepId, - ); - for await (const streamContent of readStreamableValue(stream)) { - if (streamContent === undefined) { - continue; - } - textArtifactObject = { - ...textArtifactObject, - ...streamContent, - }; - setExecution((prev) => { - if (prev === null || prev.status !== "running") { - return null; - } - return { - ...prev, - artifacts: prev.artifacts.map((artifact) => { - if (artifact.id !== artifactId) { - return artifact; - } - return { - ...artifact, - object: textArtifactObject, - }; - }), - }; - }); - } - const stepDurationMs = Date.now() - stepRunStartedAt; - setExecution((prev) => { - if (prev === null || prev.status !== "running") { - return null; - } - return { - ...prev, - jobExecutions: prev.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - stepExecutions: jobExecution.stepExecutions.map( - (stepExecution) => { - if (stepExecution.id !== currentStepExecution.id) { - return stepExecution; - } - return { - ...stepExecution, - status: "completed", - runStartedAt: stepRunStartedAt, - durationMs: stepDurationMs, - }; - }, - ), - }; - }), - artifacts: prev.artifacts.map((artifact) => { - if (artifact.id !== artifactId) { - return artifact; - } - return { - id: artifactId, - type: "generatedArtifact", - creatorNodeId: currentStepExecution.nodeId, - createdAt: Date.now(), - object: textArtifactObject, - }; - }), - }; - }); - jobDurationMs += stepDurationMs; - }, - ), + // Initialize flow execution + setExecution(createInitialExecution(flowId, executionId, jobExecutions)); + + let totalFlowDurationMs = 0; + + // Execute jobs sequentially + for (const jobExecution of jobExecutions) { + const jobDurationMs = await executeJob( + flowId, + executionId, + jobExecution, + executeStepAction, + setExecution, ); - setExecution((prev) => { - if (prev === null) { - return null; - } - return { - ...prev, - jobExecutions: prev.jobExecutions.map((jobExecution) => { - if (jobExecution.id !== currentJobExecution.id) { - return jobExecution; - } - return { - ...jobExecution, - status: "completed", - runStartedAt: jobRunStartedAt, - durationMs: jobDurationMs, - }; - }), - }; - }); - flowDurationMs += jobDurationMs; + totalFlowDurationMs += jobDurationMs; } + + // Complete flow execution setExecution((prev) => { - if (prev === null || prev.status !== "running") { - return null; - } + if (!prev || prev.status !== "running") return null; return { ...prev, status: "completed", runStartedAt: flowRunStartedAt, - durationMs: flowDurationMs, + durationMs: totalFlowDurationMs, resultArtifact: prev.artifacts[prev.artifacts.length - 1], }; }); diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index ae7fc35a..5cdc62ba 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -281,6 +281,7 @@ interface ExecutionBase { id: ExecutionId; flowId?: FlowId; jobExecutions: JobExecution[]; + artifacts: Artifact[]; } interface PendingExecution extends ExecutionBase { status: "pending"; @@ -288,13 +289,11 @@ interface PendingExecution extends ExecutionBase { interface RunningExecution extends ExecutionBase { status: "running"; runStartedAt: number; - artifacts: Artifact[]; } interface CompletedExecution extends ExecutionBase { status: "completed"; runStartedAt: number; durationMs: number; - artifacts: Artifact[]; resultArtifact: Artifact; } export type Execution = From 6565225f6ac9aba8efd88c1dd7f34a2f1aa9f101 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 20:58:05 +0900 Subject: [PATCH 21/28] fix(execution): Pass current artifacts to step execution Fix artifact resolution during step execution: - Add artifacts parameter to executeStep function signature - Pass current execution artifacts instead of global graph artifacts - Track artifacts locally to ensure consistent state - Update server action to handle new parameter Fixes bug where steps couldn't access artifacts from previous steps in the same execution run. --- .../p/[agentId]/canary/contexts/execution.tsx | 19 ++++++++++++++++++- .../p/[agentId]/canary/lib/execution.ts | 3 ++- app/(playground)/p/[agentId]/canary/page.tsx | 4 +++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index f6227ec2..da40f733 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -16,6 +16,7 @@ import { toErrorWithMessage, } from "../lib/utils"; import type { + Artifact, ArtifactId, Execution, ExecutionId, @@ -84,6 +85,7 @@ const executeStep = async ( flowId: FlowId, executionId: ExecutionId, stepExecution: StepExecution, + artifacts: Artifact[], executeStepAction: ExecuteStepAction, updateExecution: ( updater: (prev: Execution | null) => Execution | null, @@ -127,6 +129,7 @@ const executeStep = async ( flowId, executionId, stepExecution.stepId, + artifacts, ); const finalArtifact = await processStreamContent(stream, (content) => { updateExecution((prev) => { @@ -182,6 +185,7 @@ const executeJob = async ( flowId: FlowId, executionId: ExecutionId, jobExecution: JobExecution, + artifacts: Artifact[], executeStepAction: ExecuteStepAction, updateExecution: ( updater: (prev: Execution | null) => Execution | null, @@ -209,6 +213,7 @@ const executeJob = async ( flowId, executionId, step, + artifacts, executeStepAction, updateExecution, ), @@ -255,6 +260,7 @@ type ExecuteStepAction = ( flowId: FlowId, executionId: ExecutionId, stepId: StepId, + artifacts: Artifact[], ) => Promise>; interface ExecutionProviderProps { children: ReactNode; @@ -379,6 +385,7 @@ export function ExecutionProvider({ setExecution(createInitialExecution(flowId, executionId, jobExecutions)); let totalFlowDurationMs = 0; + let currentArtifacts: Artifact[] = []; // Execute jobs sequentially for (const jobExecution of jobExecutions) { @@ -386,8 +393,18 @@ export function ExecutionProvider({ flowId, executionId, jobExecution, + currentArtifacts, executeStepAction, - setExecution, + (updater) => { + // Update both state and our local artifacts array + setExecution((prev) => { + const updated = updater(prev); + if (updated) { + currentArtifacts = updated.artifacts; // Keep local array in sync + } + return updated; + }); + }, ); totalFlowDurationMs += jobDurationMs; } diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts index 95861257..73357e32 100644 --- a/app/(playground)/p/[agentId]/canary/lib/execution.ts +++ b/app/(playground)/p/[agentId]/canary/lib/execution.ts @@ -227,6 +227,7 @@ export async function executeStep( flowId: FlowId, executionId: ExecutionId, stepId: StepId, + artifacts: Artifact[], ) { const lf = new Langfuse(); const trace = lf.trace({ @@ -270,7 +271,7 @@ export async function executeStep( return node; } function artifactResolver(artifactCreatorNodeId: NodeId) { - const generatedArtifact = graph.artifacts.find( + const generatedArtifact = artifacts.find( (artifact) => artifact.creatorNodeId === artifactCreatorNodeId, ); if ( diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index 80781e47..efe9e855 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -20,6 +20,7 @@ import { isLatestVersion, migrateGraph } from "./lib/graph"; import { buildGraphFolderPath } from "./lib/utils"; import type { AgentId, + Artifact, ArtifactId, ExecutionId, FlowId, @@ -106,9 +107,10 @@ export default async function Page({ flowId: FlowId, executionId: ExecutionId, stepId: StepId, + artifacts: Artifact[], ) { "use server"; - return await executeStep(agentId, flowId, executionId, stepId); + return await executeStep(agentId, flowId, executionId, stepId, artifacts); } return ( From 95ed96b5cfb7934eba908498307ec6d0f6b1aa50 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 21:10:26 +0900 Subject: [PATCH 22/28] feat(viewer): Display step execution content with Markdown Add content display to execution viewer: - Show Markdown-rendered content for completed steps - Display pending state for steps without artifacts - Link artifacts to step executions for rendering - Add Markdown component integration Improves execution viewer by showing actual step output instead of raw execution details. --- .../p/[agentId]/canary/components/viewer.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/(playground)/p/[agentId]/canary/components/viewer.tsx b/app/(playground)/p/[agentId]/canary/components/viewer.tsx index 16b40c30..b1c13fce 100644 --- a/app/(playground)/p/[agentId]/canary/components/viewer.tsx +++ b/app/(playground)/p/[agentId]/canary/components/viewer.tsx @@ -10,6 +10,7 @@ import type { Execution, Node, StepExecution } from "../types"; import bg from "./bg.png"; import { ContentTypeIcon } from "./content-type-icon"; import { Header } from "./header"; +import { Markdown } from "./markdown"; import { EmptyState } from "./ui/empty-state"; interface StepExecutionButtonProps @@ -71,9 +72,13 @@ function ExecutionViewer({ if (node === undefined) { return null; } + const artifact = tmpExecution.artifacts.find((artifact) => { + return artifact.creatorNodeId === node.id; + }); return { ...stepExecution, node, + artifact, }; }) .filter((stepExecution) => stepExecution !== null), @@ -114,7 +119,11 @@ function ExecutionViewer({ {execution.jobExecutions.flatMap((jobExecution) => jobExecution.stepExecutions.map((stepExecution) => ( - {stepExecution.id},{stepExecution.status} + {stepExecution.artifact == null ? ( +

Pending

+ ) : ( + {stepExecution.artifact.object.content} + )}
)), )} From 5fc1127c7d1e982215868c7076fe620edd0fe995 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Thu, 12 Dec 2024 22:00:57 +0900 Subject: [PATCH 23/28] fix(execution): Correct artifact state management and resolution Fix execution state synchronization and artifact resolution: - Track execution state locally to prevent race conditions - Fix artifact type check logic in resolver - Improve execution state updates with local reference - Add debug logging for job execution tracking Fixes issues with artifact resolution during concurrent step execution and ensures consistent state updates. --- .../p/[agentId]/canary/contexts/execution.tsx | 25 +++++++++++-------- .../p/[agentId]/canary/lib/execution.ts | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index da40f733..ae61112f 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -149,6 +149,7 @@ const executeStep = async ( const stepDurationMs = Date.now() - stepRunStartedAt; updateExecution((prev) => { if (!prev || prev.status !== "running") return null; + return { ...prev, jobExecutions: prev.jobExecutions.map((job) => ({ @@ -382,28 +383,30 @@ export function ExecutionProvider({ const flowRunStartedAt = Date.now(); // Initialize flow execution - setExecution(createInitialExecution(flowId, executionId, jobExecutions)); + let currentExecution = createInitialExecution( + flowId, + executionId, + jobExecutions, + ); + setExecution(currentExecution); let totalFlowDurationMs = 0; - let currentArtifacts: Artifact[] = []; // Execute jobs sequentially for (const jobExecution of jobExecutions) { + console.log(`job: ${jobExecution.id}`); const jobDurationMs = await executeJob( flowId, executionId, jobExecution, - currentArtifacts, + currentExecution.artifacts, executeStepAction, (updater) => { - // Update both state and our local artifacts array - setExecution((prev) => { - const updated = updater(prev); - if (updated) { - currentArtifacts = updated.artifacts; // Keep local array in sync - } - return updated; - }); + const updated = updater(currentExecution); + if (updated) { + currentExecution = updated; + setExecution(updated); + } }, ); totalFlowDurationMs += jobDurationMs; diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts index 73357e32..068498ea 100644 --- a/app/(playground)/p/[agentId]/canary/lib/execution.ts +++ b/app/(playground)/p/[agentId]/canary/lib/execution.ts @@ -276,7 +276,7 @@ export async function executeStep( ); if ( generatedArtifact === undefined || - generatedArtifact.type === "generatedArtifact" + generatedArtifact.type !== "generatedArtifact" ) { return null; } From fbd8574703f07d12fa15978e12067b80951b6bb1 Mon Sep 17 00:00:00 2001 From: toyamarinyon Date: Fri, 13 Dec 2024 00:49:05 +0900 Subject: [PATCH 24/28] feat(execution): Enhance execution tracking and storage - Add functionality to store execution data using putExecutionAction - Update ExecutionProvider to handle status updates and current execution - Introduce executionIndexes in Graph and initialization logic This change improves execution tracking by allowing execution data to be persisted with an associated blob URL. The data structure has been enhanced to accommodate storage indexes, facilitating efficient retrieval of execution histories. This may impact existing storage mechanisms, so review any dependencies on execution data structures. --- .../p/[agentId]/canary/contexts/execution.tsx | 25 +++++++++++-------- .../p/[agentId]/canary/lib/graph.ts | 1 + .../p/[agentId]/canary/lib/utils.ts | 13 ++++++++++ app/(playground)/p/[agentId]/canary/page.tsx | 16 ++++++++++-- app/(playground)/p/[agentId]/canary/types.ts | 7 ++++++ 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx index ae61112f..541c20d1 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/execution.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/execution.tsx @@ -270,12 +270,14 @@ interface ExecutionProviderProps { nodeId: NodeId, ) => Promise>; executeStepAction: ExecuteStepAction; + putExecutionAction: (execution: Execution) => Promise; } export function ExecutionProvider({ children, executeAction, executeStepAction, + putExecutionAction, }: ExecutionProviderProps) { const { dispatch, flush, graph } = useGraph(); const { setTab } = usePropertiesPanel(); @@ -412,19 +414,20 @@ export function ExecutionProvider({ totalFlowDurationMs += jobDurationMs; } + currentExecution = { + ...currentExecution, + status: "completed", + runStartedAt: flowRunStartedAt, + durationMs: totalFlowDurationMs, + resultArtifact: + currentExecution.artifacts[currentExecution.artifacts.length - 1], + }; + // Complete flow execution - setExecution((prev) => { - if (!prev || prev.status !== "running") return null; - return { - ...prev, - status: "completed", - runStartedAt: flowRunStartedAt, - durationMs: totalFlowDurationMs, - resultArtifact: prev.artifacts[prev.artifacts.length - 1], - }; - }); + setExecution(currentExecution); + putExecutionAction(currentExecution); }, - [setPlaygroundMode, graph.flows, executeStepAction], + [setPlaygroundMode, graph.flows, executeStepAction, putExecutionAction], ); return ( diff --git a/app/(playground)/p/[agentId]/canary/lib/graph.ts b/app/(playground)/p/[agentId]/canary/lib/graph.ts index b7569ac5..cefee005 100644 --- a/app/(playground)/p/[agentId]/canary/lib/graph.ts +++ b/app/(playground)/p/[agentId]/canary/lib/graph.ts @@ -312,6 +312,7 @@ export function migrateGraph(graph: Graph): Graph { ...newGraph, version: "20241212", flows: deriveFlows(newGraph), + executionIndexes: [], }; } diff --git a/app/(playground)/p/[agentId]/canary/lib/utils.ts b/app/(playground)/p/[agentId]/canary/lib/utils.ts index 4c6e9e55..174f726b 100644 --- a/app/(playground)/p/[agentId]/canary/lib/utils.ts +++ b/app/(playground)/p/[agentId]/canary/lib/utils.ts @@ -176,6 +176,7 @@ export function initGraph(): Graph { artifacts: [], version: "20241212" satisfies LatestGraphVersion, flows: [], + executionIndexes: [], }; } @@ -185,6 +186,18 @@ export function buildGraphFolderPath(graphId: GraphId) { export function buildGraphPath(graphId: GraphId) { return pathJoin(buildGraphFolderPath(graphId), "graph.json"); } +function buildGraphExecutionFolderPath(graphId: GraphId) { + return pathJoin(buildGraphFolderPath(graphId), "executions"); +} +export function buildGraphExecutionPath( + graphId: GraphId, + executionId: ExecutionId, +) { + return pathJoin( + buildGraphExecutionFolderPath(graphId), + `${executionId}.json`, + ); +} export function langfuseModel(llm: TextGenerateActionContent["llm"]) { const [_, model] = llm.split(":"); diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index efe9e855..42dddfc7 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -1,6 +1,6 @@ import { agents, db } from "@/drizzle"; import { developerFlag, playgroundV2Flag } from "@/flags"; -import { del, list } from "@vercel/blob"; +import { del, list, put } from "@vercel/blob"; import { ReactFlowProvider } from "@xyflow/react"; import { eq } from "drizzle-orm"; import { notFound } from "next/navigation"; @@ -17,11 +17,12 @@ import { ToastProvider } from "./contexts/toast"; import { ToolbarContextProvider } from "./contexts/toolbar"; import { executeStep } from "./lib/execution"; import { isLatestVersion, migrateGraph } from "./lib/graph"; -import { buildGraphFolderPath } from "./lib/utils"; +import { buildGraphExecutionPath, buildGraphFolderPath } from "./lib/utils"; import type { AgentId, Artifact, ArtifactId, + Execution, ExecutionId, FlowId, Graph, @@ -112,6 +113,16 @@ export default async function Page({ "use server"; return await executeStep(agentId, flowId, executionId, stepId, artifacts); } + async function putExecutionAction(execution: Execution) { + "use server"; + await put( + buildGraphExecutionPath(graph.id, execution.id), + JSON.stringify(execution), + { + access: "public", + }, + ); + } return ( @@ -133,6 +144,7 @@ export default async function Page({ diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index 5cdc62ba..50a82b0f 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -175,6 +175,7 @@ export interface Graph { artifacts: Artifact[]; version: GraphVersion; flows: Flow[]; + executionIndexes: ExecutionIndex[]; } interface ToolBase { @@ -300,3 +301,9 @@ export type Execution = | PendingExecution | RunningExecution | CompletedExecution; + +interface ExecutionIndex { + executionId: ExecutionId; + blobUrl: string; + completedAt: number; +} From 7c55e98a063c57a3909c1246ecb90cbaaea6c4dc Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Fri, 13 Dec 2024 10:06:43 +0900 Subject: [PATCH 25/28] chore(deps): Upgrade AI SDK packages for compatibility --- app/(playground)/p/[agentId]/canary/lib/execution.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/lib/execution.ts b/app/(playground)/p/[agentId]/canary/lib/execution.ts index 068498ea..6e122d66 100644 --- a/app/(playground)/p/[agentId]/canary/lib/execution.ts +++ b/app/(playground)/p/[agentId]/canary/lib/execution.ts @@ -46,7 +46,7 @@ function resolveLanguageModel( defaultObjectGenerationMode: "json", doStream: async () => ({ stream: simulateReadableStream({ - values: [{ type: "error", error: "a" }], + chunks: [{ type: "error", error: "a" }], }), rawCall: { rawPrompt: null, rawSettings: {} }, }), @@ -316,7 +316,7 @@ export async function executeStep( }, }); (async () => { - const { partialObjectStream, object } = await streamObject({ + const { partialObjectStream, object } = streamObject({ model, prompt, schema: jsonSchema>( From 9b86b7810f61f9174470a18f847beb78a37ddb1b Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Fri, 13 Dec 2024 10:06:59 +0900 Subject: [PATCH 26/28] refactor(playground): Remove unused upsertArtifact action creator Delete the upsertArtifact helper function since it's no longer being used in the graph context. The UpsertArtifactAction type is still maintained as part of the GraphAction union type. --- app/(playground)/p/[agentId]/canary/contexts/graph.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx index 1555a4ad..a9e1fa17 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx @@ -95,15 +95,6 @@ type GraphAction = | AddNodeAction | RemoveNodeAction; -export function upsertArtifact( - input: UpsertArtifactActionInput, -): UpsertArtifactAction { - return { - type: "upsertArtifact", - input, - }; -} - type GraphActionOrActions = GraphAction | GraphAction[]; function applyActions( From 67c78c3eec3affa5714501007689031aa9338bbc Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Fri, 13 Dec 2024 11:14:52 +0900 Subject: [PATCH 27/28] feat(playground): Add execution history and metadata display Implement execution history tracking and enhance viewer UI with metadata display for generated artifacts. This change introduces persistence of execution results and UI improvements. - Add execution history tracking via executionIndexes - Display generation timestamp and add copy button for artifacts - Upgrade graph version to 20241213 to support execution history - Fix scroll behavior in execution viewer - Add blob URL storage for completed executions BREAKING: Requires graph version migration to 20241213 --- .../p/[agentId]/canary/components/viewer.tsx | 22 ++++++++++++++-- .../p/[agentId]/canary/contexts/execution.tsx | 25 ++++++++++++++++--- .../p/[agentId]/canary/contexts/graph.tsx | 18 ++++++++++++- .../p/[agentId]/canary/lib/graph.ts | 10 +++++--- .../p/[agentId]/canary/lib/utils.ts | 2 +- app/(playground)/p/[agentId]/canary/page.tsx | 3 ++- app/(playground)/p/[agentId]/canary/types.ts | 11 +++++--- 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/components/viewer.tsx b/app/(playground)/p/[agentId]/canary/components/viewer.tsx index b1c13fce..0cd0c6e2 100644 --- a/app/(playground)/p/[agentId]/canary/components/viewer.tsx +++ b/app/(playground)/p/[agentId]/canary/components/viewer.tsx @@ -6,8 +6,10 @@ import { SpinnerIcon } from "../../beta-proto/components/icons/spinner"; import { WilliIcon } from "../../beta-proto/components/icons/willi"; import { useExecution } from "../contexts/execution"; import { useGraph } from "../contexts/graph"; +import { formatTimestamp } from "../lib/utils"; import type { Execution, Node, StepExecution } from "../types"; import bg from "./bg.png"; +import ClipboardButton from "./clipboard-button"; import { ContentTypeIcon } from "./content-type-icon"; import { Header } from "./header"; import { Markdown } from "./markdown"; @@ -115,7 +117,7 @@ function ExecutionViewer({ ))}
-
+
{execution.jobExecutions.flatMap((jobExecution) => jobExecution.stepExecutions.map((stepExecution) => ( @@ -124,6 +126,22 @@ function ExecutionViewer({ ) : ( {stepExecution.artifact.object.content} )} + {stepExecution.artifact?.type === "generatedArtifact" && ( +
+
+ Generated{" "} + {formatTimestamp.toRelativeTime( + stepExecution.artifact.createdAt, + )} +
+
+ +
+
+ )}
)), )} @@ -211,7 +229,7 @@ export function Viewer() { }} >
-
+
{execution === null ? ( Promise>; executeStepAction: ExecuteStepAction; - putExecutionAction: (execution: Execution) => Promise; + putExecutionAction: (execution: Execution) => Promise<{ blobUrl: string }>; } export function ExecutionProvider({ @@ -380,6 +380,7 @@ export function ExecutionProvider({ if (!flow) throw new Error("Flow not found"); setPlaygroundMode("viewer"); + await flush(); const executionId = createExecutionId(); const jobExecutions = createInitialJobExecutions(flow); const flowRunStartedAt = Date.now(); @@ -396,7 +397,6 @@ export function ExecutionProvider({ // Execute jobs sequentially for (const jobExecution of jobExecutions) { - console.log(`job: ${jobExecution.id}`); const jobDurationMs = await executeJob( flowId, executionId, @@ -425,9 +425,26 @@ export function ExecutionProvider({ // Complete flow execution setExecution(currentExecution); - putExecutionAction(currentExecution); + const { blobUrl } = await putExecutionAction(currentExecution); + dispatch({ + type: "addExecutionIndex", + input: { + executionIndex: { + executionId, + blobUrl, + completedAt: Date.now(), + }, + }, + }); }, - [setPlaygroundMode, graph.flows, executeStepAction, putExecutionAction], + [ + setPlaygroundMode, + graph.flows, + executeStepAction, + putExecutionAction, + dispatch, + flush, + ], ); return ( diff --git a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx index a9e1fa17..89b0f2e9 100644 --- a/app/(playground)/p/[agentId]/canary/contexts/graph.tsx +++ b/app/(playground)/p/[agentId]/canary/contexts/graph.tsx @@ -14,6 +14,7 @@ import type { Artifact, Connection, ConnectionId, + ExecutionIndex, Graph, Node, NodeHandleId, @@ -85,6 +86,12 @@ interface RemoveNodeAction { type: "removeNode"; input: RemoveNoeActionInput; } + +interface AddExecutionIndexAction { + type: "addExecutionIndex"; + input: { executionIndex: ExecutionIndex }; +} + type GraphAction = | UpsertArtifactAction | UpdateNodeAction @@ -93,7 +100,8 @@ type GraphAction = | UpdateNodePositionAction | UpdateNodeSelectionAction | AddNodeAction - | RemoveNodeAction; + | RemoveNodeAction + | AddExecutionIndexAction; type GraphActionOrActions = GraphAction | GraphAction[]; @@ -197,6 +205,14 @@ function graphReducer(graph: Graph, action: GraphAction): Graph { ...graph, nodes: graph.nodes.filter((node) => node.id !== action.input.nodeId), }; + case "addExecutionIndex": + return { + ...graph, + executionIndexes: [ + ...graph.executionIndexes, + action.input.executionIndex, + ], + }; default: return graph; } diff --git a/app/(playground)/p/[agentId]/canary/lib/graph.ts b/app/(playground)/p/[agentId]/canary/lib/graph.ts index cefee005..d00fd842 100644 --- a/app/(playground)/p/[agentId]/canary/lib/graph.ts +++ b/app/(playground)/p/[agentId]/canary/lib/graph.ts @@ -268,7 +268,7 @@ export function deriveFlows(graph: Graph): Flow[] { } export function isLatestVersion(graph: Graph): boolean { - return graph.version === "20241212"; + return graph.version === "20241213"; } export function migrateGraph(graph: Graph): Graph { @@ -307,10 +307,14 @@ export function migrateGraph(graph: Graph): Graph { }; } - if (newGraph.version === "2024-12-10" || newGraph.version === "2024-12-11") { + if ( + newGraph.version === "2024-12-10" || + newGraph.version === "2024-12-11" || + newGraph.version === "20241212" + ) { newGraph = { ...newGraph, - version: "20241212", + version: "20241213", flows: deriveFlows(newGraph), executionIndexes: [], }; diff --git a/app/(playground)/p/[agentId]/canary/lib/utils.ts b/app/(playground)/p/[agentId]/canary/lib/utils.ts index 174f726b..23ba7c7c 100644 --- a/app/(playground)/p/[agentId]/canary/lib/utils.ts +++ b/app/(playground)/p/[agentId]/canary/lib/utils.ts @@ -174,7 +174,7 @@ export function initGraph(): Graph { nodes: [], connections: [], artifacts: [], - version: "20241212" satisfies LatestGraphVersion, + version: "20241213" satisfies LatestGraphVersion, flows: [], executionIndexes: [], }; diff --git a/app/(playground)/p/[agentId]/canary/page.tsx b/app/(playground)/p/[agentId]/canary/page.tsx index 42dddfc7..5220a1f7 100644 --- a/app/(playground)/p/[agentId]/canary/page.tsx +++ b/app/(playground)/p/[agentId]/canary/page.tsx @@ -115,13 +115,14 @@ export default async function Page({ } async function putExecutionAction(execution: Execution) { "use server"; - await put( + const result = await put( buildGraphExecutionPath(graph.id, execution.id), JSON.stringify(execution), { access: "public", }, ); + return { blobUrl: result.url }; } return ( diff --git a/app/(playground)/p/[agentId]/canary/types.ts b/app/(playground)/p/[agentId]/canary/types.ts index 50a82b0f..68d2150b 100644 --- a/app/(playground)/p/[agentId]/canary/types.ts +++ b/app/(playground)/p/[agentId]/canary/types.ts @@ -166,8 +166,13 @@ interface TextStreamArtifact extends StreamAtrifact { export type Artifact = TextArtifact | TextStreamArtifact; export type GraphId = `grph_${string}`; -type GraphVersion = "2024-12-09" | "2024-12-10" | "2024-12-11" | "20241212"; -export type LatestGraphVersion = "20241212"; +type GraphVersion = + | "2024-12-09" + | "2024-12-10" + | "2024-12-11" + | "20241212" + | "20241213"; +export type LatestGraphVersion = "20241213"; export interface Graph { id: GraphId; nodes: Node[]; @@ -302,7 +307,7 @@ export type Execution = | RunningExecution | CompletedExecution; -interface ExecutionIndex { +export interface ExecutionIndex { executionId: ExecutionId; blobUrl: string; completedAt: number; From ff471644cfb91437a1d70fb22d30e5fa4b5c6c97 Mon Sep 17 00:00:00 2001 From: satoshi toyama Date: Fri, 13 Dec 2024 11:29:47 +0900 Subject: [PATCH 28/28] pass test --- app/(playground)/p/[agentId]/canary/lib/graph.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(playground)/p/[agentId]/canary/lib/graph.test.ts b/app/(playground)/p/[agentId]/canary/lib/graph.test.ts index 89445662..194d9c9f 100644 --- a/app/(playground)/p/[agentId]/canary/lib/graph.test.ts +++ b/app/(playground)/p/[agentId]/canary/lib/graph.test.ts @@ -136,7 +136,7 @@ describe("deriveFlows", () => { describe("isLatestVersion", () => { test("latest version", () => { - expect(isLatestVersion({ version: "2024-12-10" } as Graph)).toBe(true); + expect(isLatestVersion({ version: "20241213" } as Graph)).toBe(true); }); test("old version", () => { expect(isLatestVersion({} as Graph)).toBe(false); @@ -182,7 +182,7 @@ describe("migrateGraph", () => { ], artifacts: [], } as unknown as Graph); - expect(after.version).toBe("2024-12-10"); + expect(after.version).toBe("20241213"); expect(after.nodes[0].content.type).toBe("files"); }); });