Browse Source

feat(35-01): implement binary stripping and rich workflow context

Implement stripBinaryData() utility and enhance buildWorkflowContext():
- Strip all base64 data (image/video) from all 8 node types
- Replace with metadata placeholders: [image: 1024x768, 245KB] format
- Remove history arrays (imageHistory, videoHistory, selectedHistoryIndex)
- Remove ref fields (imageRef, outputImageRef, etc.)
- Preserve all node parameters, positions, and model settings
- Enhanced WorkflowContext with full StrippedNode data
- Updated formatContextForPrompt to render rich node context

Binary fields inventory:
- imageInput: image
- annotation: sourceImage, outputImage
- nanoBanana: inputImages, outputImage
- generateVideo: inputImages, outputVideo
- llmGenerate: inputImages
- splitGrid: sourceImage
- output: image, video
- prompt: (none)

All 22 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
e9abe1c3ee
  1. 23
      .planning/STATE.md
  2. 194
      src/lib/chat/contextBuilder.ts

23
.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

194
src/lib/chat/contextBuilder.ts

@ -1,76 +1,203 @@
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<string, string[]> = {
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<string, unknown>;
}
/**
* 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;
}
/**
* Builds a lightweight workflow context from nodes and edges.
* Omits all base64 image data, history arrays, and internal state.
* 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 - Current workflow nodes
* @param edges - Current workflow edges
* @returns Lightweight workflow context suitable for LLM injection
* @param nodes - Workflow nodes to strip
* @returns Stripped nodes with binary data replaced by metadata
*/
export function buildWorkflowContext(
nodes: WorkflowNode[],
edges: WorkflowEdge[]
): WorkflowContext {
const isEmpty = nodes.length === 0;
export function stripBinaryData(nodes: WorkflowNode[]): StrippedNode[] {
return nodes.map((node) => {
const strippedData: Record<string, unknown> = {};
const binaryFields = BINARY_FIELDS_BY_TYPE[node.type] || [];
// 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);
// 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;
}
// For nanoBanana and generateVideo nodes, include model name
let model: string | undefined;
// 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;
model = nodeData.selectedModel?.displayName;
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,
title,
...(model && { model }),
position: node.position,
data: strippedData,
};
});
}
/**
* Builds a lightweight workflow context from nodes and edges.
* Omits all base64 image data, history arrays, and internal state.
*
* @param nodes - Current workflow nodes
* @param edges - Current workflow edges
* @returns Lightweight workflow context suitable for LLM injection
*/
export function buildWorkflowContext(
nodes: WorkflowNode[],
edges: WorkflowEdge[]
): WorkflowContext {
const isEmpty = nodes.length === 0;
// 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}`);
}
}

Loading…
Cancel
Save