diff --git a/.planning/STATE.md b/.planning/STATE.md index 6033ccb6..77a13e68 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -9,19 +9,19 @@ See: .planning/PROJECT.md (updated 2026-01-09) ## Current Position -Phase: 34 of 35 (Context-Aware Agentic Workflow Editing) -Plan: 3 of 3 in current phase -Status: Complete -Last activity: 2026-01-30 - Completed plan 03 (Store Integration and ChatPanel Wiring) +Phase: 35 of 35 (Large Workflow Handling) +Plan: 1 of 3 in current phase +Status: In Progress +Last activity: 2026-01-31 - Completed plan 02 (Selection-Aware Subgraph Extraction) Progress: ░░░░░░░░░░ 6% ## Performance Metrics **Velocity:** -- Total plans completed: 33 +- Total plans completed: 34 - Average duration: 7 min -- Total execution time: 3.7 hours +- Total execution time: 3.9 hours **By Phase:** @@ -56,10 +56,11 @@ Progress: ░░░░░░░░░░ 6% | 32. Chat UI Foundation | 2/2 | 9 min | 4.5 min | | 33. Workflow Edit Safety | 2/2 | 5 min | 5 min | | 34. Agentic Workflow Editing | 3/3 | 13 min | 4.3 min | +| 35. Large Workflow Handling | 1/3 | 10 min | 10 min | **Recent Trend:** -- Last 5 plans: 5 min, 5 min, 5 min, 8 min, <1 min -- Trend: Phase 34 complete - agentic workflow editing shipped in 3 compact plans +- Last 5 plans: 5 min, 5 min, 8 min, <1 min, 10 min +- Trend: Phase 35 started - selection-aware context scoping for large workflows ## Accumulated Context @@ -206,7 +207,7 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-01-30 -Stopped at: Completed Phase 34 Plan 03 (Store Integration and ChatPanel Wiring) +Last session: 2026-01-31 +Stopped at: Completed Phase 35 Plan 02 (Selection-Aware Subgraph Extraction) Resume file: None -Next action: Phase 34 complete - proceed to next phase or milestone planning +Next action: Proceed to plan 35-03 (Chat Context Builder Integration) or continue with parallel plan 35-01 diff --git a/src/lib/chat/contextBuilder.ts b/src/lib/chat/contextBuilder.ts index fabe3d90..1e5eaebd 100644 --- a/src/lib/chat/contextBuilder.ts +++ b/src/lib/chat/contextBuilder.ts @@ -1,30 +1,175 @@ import { WorkflowNode } from "@/types"; import { WorkflowEdge } from "@/types/workflow"; import type { + ImageInputNodeData, + AnnotationNodeData, + PromptNodeData, NanoBananaNodeData, GenerateVideoNodeData, + LLMGenerateNodeData, + SplitGridNodeData, + OutputNodeData, } from "@/types"; +/** + * Binary fields by node type - fields containing base64 data URLs + */ +const BINARY_FIELDS_BY_TYPE: Record = { + imageInput: ["image"], + annotation: ["sourceImage", "outputImage"], + nanoBanana: ["inputImages", "outputImage"], + generateVideo: ["inputImages", "outputVideo"], + llmGenerate: ["inputImages"], + splitGrid: ["sourceImage"], + output: ["image", "video"], + prompt: [], +}; + +/** + * History fields to strip completely (irrelevant for editing context) + */ +const HISTORY_FIELDS = [ + "imageHistory", + "videoHistory", + "selectedHistoryIndex", + "selectedVideoHistoryIndex", +]; + +/** + * Ref fields to strip completely (internal storage tracking) + */ +const REF_FIELDS = [ + "imageRef", + "outputImageRef", + "sourceImageRef", + "inputImageRefs", + "outputVideoRef", +]; + +/** + * Stripped node with binary data replaced by metadata placeholders + */ +export interface StrippedNode { + id: string; + type: string; + position: { x: number; y: number }; + data: Record; +} + /** * Lightweight workflow context for LLM consumption. * Strips all base64 image data, history arrays, and internal state. */ export interface WorkflowContext { nodeCount: number; - nodes: Array<{ - id: string; - type: string; - title: string; - model?: string; - }>; + nodes: StrippedNode[]; connections: Array<{ from: string; to: string; - type: string; + sourceHandle: string | null; + targetHandle: string | null; }>; isEmpty: boolean; } +/** + * Estimates the size of a base64 data URL in kilobytes. + */ +function estimateBase64Size(dataUrl: string): number { + // Base64 encoding overhead: every 3 bytes becomes 4 characters + // So to get original size: (length * 3) / 4 + return Math.round((dataUrl.length * 3) / 4 / 1024); +} + +/** + * Formats a binary field placeholder with metadata. + */ +function formatBinaryPlaceholder( + value: string | string[], + fieldName: string, + context?: string +): string { + // Handle array fields (inputImages) + if (Array.isArray(value)) { + if (value.length === 0) { + return "[no images]"; + } + const count = value.length; + return `[${count} image(s)]`; + } + + // Handle single string fields + const sizeKB = estimateBase64Size(value); + const isVideo = fieldName.toLowerCase().includes("video"); + const type = isVideo ? "video" : "image"; + + // Format: [type: context, sizeKB] or [type: sizeKB] if no context + if (context) { + return `[${type}: ${context}, ${sizeKB}KB]`; + } + return `[${type}: ${sizeKB}KB]`; +} + +/** + * Strips binary data from workflow nodes, preserving all parameters and adding metadata placeholders. + * + * @param nodes - Workflow nodes to strip + * @returns Stripped nodes with binary data replaced by metadata + */ +export function stripBinaryData(nodes: WorkflowNode[]): StrippedNode[] { + return nodes.map((node) => { + const strippedData: Record = {}; + const binaryFields = BINARY_FIELDS_BY_TYPE[node.type] || []; + + // Copy all data fields except binary, history, and ref fields + for (const [key, value] of Object.entries(node.data)) { + // Skip history fields + if (HISTORY_FIELDS.includes(key)) { + continue; + } + // Skip ref fields + if (REF_FIELDS.includes(key)) { + continue; + } + + // Handle binary fields + if (binaryFields.includes(key)) { + if (value === null || value === undefined) { + strippedData[key] = value; + } else { + // Add context based on node type + let context: string | undefined; + if (node.type === "nanoBanana" || node.type === "generateVideo") { + const nodeData = node.data as NanoBananaNodeData | GenerateVideoNodeData; + context = nodeData.selectedModel?.displayName; + } else if (node.type === "imageInput") { + const nodeData = node.data as ImageInputNodeData; + if (nodeData.dimensions) { + context = `${nodeData.dimensions.width}x${nodeData.dimensions.height}`; + } + } + + strippedData[key] = formatBinaryPlaceholder( + value as string | string[], + key, + context + ); + } + } else { + // Copy non-binary field as-is + strippedData[key] = value; + } + } + + return { + id: node.id, + type: node.type, + position: node.position, + data: strippedData, + }; + }); +} + /** * Builds a lightweight workflow context from nodes and edges. * Omits all base64 image data, history arrays, and internal state. @@ -39,38 +184,20 @@ export function buildWorkflowContext( ): WorkflowContext { const isEmpty = nodes.length === 0; - // Map nodes to lightweight summaries - const nodeSummaries = nodes.map((node) => { - // Extract title: use customTitle if available, otherwise generate from type - const title = - (node.data as { customTitle?: string }).customTitle || - generateNodeTitle(node.type); - - // For nanoBanana and generateVideo nodes, include model name - let model: string | undefined; - if (node.type === "nanoBanana" || node.type === "generateVideo") { - const nodeData = node.data as NanoBananaNodeData | GenerateVideoNodeData; - model = nodeData.selectedModel?.displayName; - } - - return { - id: node.id, - type: node.type, - title, - ...(model && { model }), - }; - }); + // Strip binary data from all nodes + const strippedNodes = stripBinaryData(nodes); // Map edges to connection summaries const connections = edges.map((edge) => ({ from: edge.source, to: edge.target, - type: edge.sourceHandle || "unknown", + sourceHandle: edge.sourceHandle || null, + targetHandle: edge.targetHandle || null, })); return { nodeCount: nodes.length, - nodes: nodeSummaries, + nodes: strippedNodes, connections, isEmpty, }; @@ -92,8 +219,8 @@ export function formatContextForPrompt(context: WorkflowContext): string { // List nodes lines.push(`Current workflow has ${context.nodeCount} node(s):`); for (const node of context.nodes) { - const modelInfo = node.model ? ` (${node.model})` : ""; - lines.push(` - ${node.id}: ${node.title}${modelInfo}`); + const title = (node.data.customTitle as string) || generateNodeTitle(node.type); + lines.push(` - ${node.id}: ${title}`); } // List connections @@ -101,7 +228,8 @@ export function formatContextForPrompt(context: WorkflowContext): string { lines.push(""); lines.push("Connections:"); for (const conn of context.connections) { - lines.push(` - ${conn.from} → ${conn.to} (${conn.type})`); + const handleInfo = conn.sourceHandle ? ` (${conn.sourceHandle})` : ""; + lines.push(` - ${conn.from} → ${conn.to}${handleInfo}`); } }