diff --git a/charm/src/IMAGINE-DESIGN.md b/charm/src/IMAGINE-DESIGN.md
new file mode 100644
index 000000000..db8f05092
--- /dev/null
+++ b/charm/src/IMAGINE-DESIGN.md
@@ -0,0 +1,299 @@
+# Charm Workflow System: A Comprehensive Guide
+
+## Overview
+
+The Charm Workflow System provides an LLM-powered approach to charm generation and modification. It analyzes user intent, classifies operations into specific workflows, and generates appropriate code through a structured pipeline. The system is designed to be predictable, maintainable, and extensible.
+
+## Code Execution Flow
+
+```mermaid
+flowchart TD
+ %% Entry Point
+ start([UI: user input]) --> A["imagine/modifyCharm"]
+ A --> B["executeWorkflow"]
+
+ %% Main processing function
+ B --> C["processWorkflow"]
+
+ %% 1. FORMAT stage
+ C --> F1["1. FORMAT: processInputSection"]
+ F1 --> FM["formatPromptWithMentions"]
+ FM --> DP1[/"Form Packet 1:
+ - rawInput
+ - processedInput
+ - references"/]
+
+ %% 2. CLASSIFY stage
+ DP1 --> CL1["2. CLASSIFY: fillClassificationSection"]
+ CL1 --> CL2["classifyIntent"]
+ CL2 --> CL3["classifyWorkflow LLM"]
+ CL3 --> DP2[/"Form Packet 2:
+ - workflowType
+ - confidence
+ - reasoning"/]
+
+ %% 3. PLAN stage
+ DP2 --> P1["3. PLAN: fillPlanningSection"]
+ P1 --> P2["generatePlan"]
+ P2 --> P3["generateWorkflowPlan LLM"]
+ P3 --> DP3[/"Form Packet 3:
+ - steps
+ - spec
+ - schema
+ - meta.isFilled = true"/]
+
+ %% 4. GENERATE CODE stage - branching point
+ DP3 --> G1["4. GENERATE: generateCode"]
+
+ %% Workflow type decision
+ G1 --> decision{Which workflow?}
+ decision -->|"fix"| FIX["executeFixWorkflow"]
+ decision -->|"edit"| EDIT["executeEditWorkflow"]
+ decision -->|"rework"| REWORK["executeReworkWorkflow"]
+
+ %% Fix & Edit Path (via iterate)
+ FIX & EDIT --> IT1["iterate"]
+ IT1 --> IT2["getIframeRecipe"]
+ IT2 --> IT3["genSrc"]
+ IT3 --> IT4["generateNewRecipeVersion"]
+ IT4 --> DP4A[/"Data Packet 4A:
+ - recipeId
+ - newRecipeSrc
+ - newSpec"/]
+
+ %% Rework Path
+ REWORK --> RW1["castNewRecipe"]
+ RW1 --> RW2["scrub/turnCellsIntoAliases"]
+ RW2 --> RW3["generateSpecAndSchema"]
+ RW3 --> RW4["genSrc"]
+ RW4 --> RW5["buildFullRecipe"]
+ RW5 --> DP4B[/"Data Packet 4B:
+ - newRecipeSrc
+ - newSpec
+ - scrubbed data"/]
+
+ %% 5. COMPILE Stage - paths converge
+ DP4A & DP4B --> CO1["5. COMPILE: compileAndRunRecipe"]
+ CO1 --> CO2["compileRecipe"]
+ CO2 --> CO3["tsToExports"]
+ CO3 --> CO4["registerNewRecipe"]
+
+ %% Result
+ CO4 --> Result[/"Final Form:
+ - generation.charm
+ - meta.isComplete = true"/]
+ Result --> final([Charm Cell])
+```
+
+### Pipeline Description
+
+The workflow system flows through distinct stages with clear data assembly at each step:
+
+1. **FORMAT** - Processing input and references
+ - `processInputSection` handles raw user input
+ - `formatPromptWithMentions` resolves @mentions to charm references
+ - **Output Packet**: Form with processed input and references
+
+2. **CLASSIFY** - Determining the workflow type
+ - `fillClassificationSection` orchestrates classification
+ - `classifyIntent` analyzes input to choose workflow type
+ - Forces "rework" workflow when other charms are referenced
+ - **Output Packet**: Form with workflow type, confidence score, and reasoning
+
+3. **PLAN** - Generating execution plan
+ - `fillPlanningSection` develops the execution plan
+ - `generatePlan` creates steps, spec updates, and schema updates
+ - Preserves existing spec for "fix" workflow
+ - **Output Packet**: Form with execution plan, spec, and schema
+
+4. **GENERATE CODE** - Branching to workflow-specific implementation
+ - Routes form to appropriate workflow function:
+
+ **4A. ITERATE** (Fix & Edit workflows)
+ - Both use `iterate()` but with different parameters
+ - Fix: Preserves spec, only updates code
+ - Edit: Updates both spec and code
+ - `generateNewRecipeVersion` creates new recipe with updates
+ - **Output Packet**: Recipe id, source, spec
+
+ **4B. REWORK** (Rework workflow)
+ - Uses `castNewRecipe()` to create entirely new charm
+ - Processes charm references with `turnCellsIntoAliases`
+ - Generates new spec and schema
+ - **Output Packet**: Recipe source, spec, processed data references
+
+5. **COMPILE** - Final recipe compilation and charm creation
+ - `compileAndRunRecipe` takes recipe source and compiles it
+ - `compileRecipe` handles TypeScript compilation
+ - Updates form with the generated charm
+ - **Output Packet**: Completed form with charm cell, ready to return
+
+The form object is progressively enriched at each stage, with each stage building on the data packet from the previous stage. The system only branches after the planning stage when the workflow type must be handled differently.
+
+## Entry Points
+
+The workflow system can be initiated from several places in the UI:
+
+1. **Command Center** (`CommandCenter.tsx`)
+ - Global command palette (⌘K)
+ - Processes user input and @mentions
+ - Shows live preview with `useLiveSpecPreview` hook
+ - Uses `handleImagineOperation()` in `commands.ts` to dispatch workflow
+ - Entry point for both new charms and modifications to existing charms
+ - Supports "Fast" vs "Smart" model selection for preview generation
+
+2. **CharmDetailView** (`CharmDetailView.tsx`)
+ - Operation tab in the bottom sheet when viewing a charm
+ - Uses `useCharmOperation()` hook to manage operations
+ - Handles iterate, extend, and variant generation workflows
+ - Performs operations via `performOperation()` function
+
+3. **Direct Functions**
+ - External applications can call the core functions directly:
+ - `executeWorkflow()`: Main entry point for all workflow processing
+ - `imagine()`: Alias for `executeWorkflow()`, used in most places
+ - `modifyCharm()`: Higher-level function for modifying existing charms
+
+## Core Workflow Types
+
+The system supports three primary workflows:
+
+1. **Fix**
+ - **Purpose**: Correct issues without changing functionality
+ - **Updates**: Code only
+ - **Schema**: Preserved
+ - **Spec**: Preserved
+ - **Example**: "Fix the alignment of the buttons"
+ - **Implementation**: Uses `iterate()` with preserved spec
+
+2. **Edit**
+ - **Purpose**: Add features while preserving core data structure
+ - **Updates**: Code and specification
+ - **Schema**: Core structure preserved
+ - **Example**: "Add dark mode support"
+ - **Implementation**: Uses `iterate()` with spec updates
+
+3. **Rework**
+ - **Purpose**: Create something new, potentially combining data sources
+ - **Updates**: Code, specification, and schema
+ - **Schema**: Can be completely different
+ - **Example**: "Create a dashboard combining my tasks and calendar"
+ - **Implementation**: Uses `castNewRecipe()`
+ - **References**: Can reference multiple charms
+
+## Workflow Form Progression
+
+The system builds up a `WorkflowForm` object throughout the process:
+
+1. **Creation**: `createWorkflowForm(rawInput)` initializes the basic structure
+ ```typescript
+ {
+ input: { rawInput, processedInput: rawInput, references: {} },
+ classification: { workflowType: "edit", confidence: 0, reasoning: "" },
+ plan: { steps: [] },
+ meta: { isComplete: false, isFilled: false }
+ }
+ ```
+
+2. **Input Processing**: `processInputSection(form, options)`
+ - Processes @mentions in the input
+ - Resolves references to other charms
+ - Updates `form.input.processedInput` and `form.input.references`
+ - Sets `charmManager` in `form.meta`
+
+3. **Classification**: `fillClassificationSection(form, options)`
+ - Determines workflow type via LLM or heuristic fallbacks
+ - Forces "rework" workflow when other charms are referenced
+ - Updates `form.classification`
+ - Sets `modelId` in `form.meta`
+
+4. **Planning**: `fillPlanningSection(form, options)`
+ - Generates execution plan and updated spec/schema
+ - For "fix" workflow, preserves existing spec
+ - Updates `form.plan` with steps, spec, and schema
+ - Sets `form.meta.isFilled = true`
+
+5. **Generation**: `generateCode(form)`
+ - Executes the appropriate workflow function
+ - Sets `form.generation.charm` with the created charm
+ - Sets `form.meta.isComplete = true`
+
+## Charm References and @Mentions
+
+When a user references another charm with an @mention:
+
+1. The `formatPromptWithMentions()` function processes these references
+2. The system forces a "rework" workflow even if manual override is attempted
+3. The references are processed in `executeReworkWorkflow()` to create a new charm with access to referenced data
+
+## UI Integration
+
+1. **Live Preview**: The `useLiveSpecPreview` hook:
+ - Processes input as the user types
+ - Calls `generateWorkflowPreview()` which runs the workflow process in "dry run" mode
+ - Provides dynamic feedback on classification, plan, and spec
+ - Manages multiple loading states for different preview components
+ - Supports "Fast" vs "Smart" model selection
+
+2. **SpecPreview Component**:
+ - Displays workflow type with confidence score
+ - Shows execution plan as numbered steps
+ - Conditionally shows spec (hidden for "fix" workflow)
+ - Allows workflow type override through a toggle
+
+## Execution Pipeline
+
+The main entry point is `executeWorkflow()`, which:
+
+1. Processes the full form with `processWorkflow(input, false, options)`
+2. Routes execution to the appropriate workflow function:
+ - `executeFixWorkflow()`: Preserves spec, updates implementation
+ - `executeEditWorkflow()`: Updates spec and implementation
+ - `executeReworkWorkflow()`: Creates new charm with new spec/schema
+
+## Integration with existing components:
+
+1. The system leverages existing functions:
+ - `iterate()`: Core function for Fix/Edit workflows
+ - `castNewRecipe()`: Core function for Rework workflow
+ - `genSrc()`: Generates source code for all workflows
+
+2. LLM integration:
+ - `classifyWorkflow()`: LLM-based classification
+ - `generateWorkflowPlan()`: LLM-based plan generation
+ - `generateSpecAndSchema()`: For creating new specifications and schemas
+
+## Fallback Mechanisms
+
+The system includes fallbacks at each stage:
+
+1. If LLM classification fails, it falls back to keyword heuristics
+2. If plan generation fails, it provides default plans for each workflow
+3. If mention processing fails, it continues with the raw input
+
+## Key Implementation Details
+
+- **Workflow Form**: Centralized data structure that evolves through the process
+- **Force Rework**: References to other charms force the rework workflow
+- **Input Processing**: Handles @mentions and creates charm references
+- **Live Preview**: Progressive processing showing classification → plan → spec
+- **Execution Plan**: Structured steps to fulfill the user's request
+
+## Extensibility
+
+The workflow system is designed to be extensible:
+
+1. **New Workflow Types**: The `WorkflowType` type and `WORKFLOWS` record can be expanded
+ - Add new entries to the `WORKFLOWS` record
+ - Update classification logic to recognize new types
+ - Implement specialized execution functions
+
+2. **Model Selection**: The system supports different LLM models for different stages
+ - Fast models for classification and preview
+ - More powerful models for code generation
+ - `getModelId()` function maps user-friendly names to model identifiers
+
+3. **UI Customization**: The workflow UI components are decoupled
+ - `SpecPreview` can be integrated in different contexts
+ - `useLiveSpecPreview` hook can be used in custom components
+ - Workflow type toggle allows manual override when appropriate
\ No newline at end of file
diff --git a/charm/src/commands.ts b/charm/src/commands.ts
index f370bae72..a9a6ec347 100644
--- a/charm/src/commands.ts
+++ b/charm/src/commands.ts
@@ -3,8 +3,18 @@ import { Cell, getRecipe } from "@commontools/runner";
import { Charm, CharmManager } from "./charm.ts";
import { getIframeRecipe } from "./iframe/recipe.ts";
import { extractUserCode, injectUserCode } from "./iframe/static.ts";
-import { compileAndRunRecipe, generateNewRecipeVersion } from "./iterate.ts";
+import {
+ castNewRecipe,
+ compileAndRunRecipe,
+ generateNewRecipeVersion,
+} from "./iterate.ts";
import { NAME } from "@commontools/builder";
+import { executeWorkflow } from "./imagine.ts";
+import {
+ ExecutionPlan,
+ formatPromptWithMentions,
+ WorkflowForm,
+} from "./index.ts";
export const castSpellAsCharm = async (
charmManager: CharmManager,
@@ -93,3 +103,82 @@ export async function addGithubRecipe(
runOptions,
);
}
+
+/**
+ * Modify a charm with the given prompt. This replaces the separate Etherate/Extend functionality.
+ * The prompt will be processed for mentions and the current charm will be included in the context.
+ * The workflow (edit, rework, fix) will be automatically determined based on the prompt.
+ *
+ * @param charmManager The CharmManager instance
+ * @param promptText The user's input describing what they want to do
+ * @param currentCharm The charm being modified
+ * @param model Optional LLM model to use
+ * @param workflowType Optional: Allow specifying workflow type (will be overridden to "rework" if references exist)
+ * @param previewPlan Optional: Pass through a pre-generated plan
+ * @returns A new or modified charm
+ */
+export function modifyCharm(
+ charmManager: CharmManager,
+ promptText: string,
+ currentCharm: Cell,
+ prefill?: Partial,
+ model?: string,
+): Promise> {
+ // Include the current charm in the context
+ const context = {
+ currentCharm: currentCharm,
+ prefill,
+ model,
+ };
+
+ return executeWorkflow(
+ charmManager,
+ promptText,
+ context,
+ );
+}
+
+/**
+ * This function is equivalent to calling modifyCharm with workflowType="rework"
+ * It exists for backward compatibility and clarity in code
+ *
+ * @param charmManager CharmManager instance
+ * @param currentCharmId ID of the charm to extend from
+ * @param goal The prompt text describing what to create
+ * @param cells Optional additional data references to include
+ * @returns A new charm that extends from the current charm
+ */
+export async function extendCharm(
+ charmManager: CharmManager,
+ currentCharmId: string,
+ goal: string,
+ cells?: Record>,
+): Promise> {
+ const charm = (await charmManager.get(currentCharmId, false))!;
+
+ // Process any cells to include as references
+ const additionalReferences: Record> = {};
+
+ if (cells && Object.keys(cells).length > 0) {
+ // Add cells to additionalReferences
+ for (const [id, cell] of Object.entries(cells)) {
+ additionalReferences[id] = cell;
+ }
+ }
+
+ const classification: WorkflowForm["classification"] = {
+ confidence: 1.0,
+ workflowType: "imagine",
+ reasoning: "Extend is always imagining since it changes argument schema",
+ };
+
+ const context = {
+ currentCharm: charm,
+ dataReferences: additionalReferences,
+ prefill: {
+ classification,
+ },
+ };
+
+ return executeWorkflow(charmManager, goal, context);
+}
diff --git a/charm/src/format.ts b/charm/src/format.ts
new file mode 100644
index 000000000..bf0380c4c
--- /dev/null
+++ b/charm/src/format.ts
@@ -0,0 +1,257 @@
+import { Module, NAME, Recipe } from "@commontools/builder";
+import { Charm, CharmManager, charmSchema } from "./index.ts";
+import { Cell, getEntityId, isCell, isStream } from "@commontools/runner";
+
+/**
+ * Converts a string of multiple words into camelCase format
+ * @param input - The string to convert
+ * @returns The camelCased string
+ *
+ * Examples:
+ * - "hello world" -> "helloWorld"
+ * - "The quick brown FOX" -> "theQuickBrownFox"
+ * - "this-is-a-test" -> "thisIsATest"
+ * - "already_camel_case" -> "alreadyCamelCase"
+ */
+function toCamelCase(input: string): string {
+ // Handle empty string case
+ if (!input) return "";
+
+ // Split the input string by non-alphanumeric characters
+ return input
+ .split(/[^a-zA-Z0-9]/)
+ .filter((word) => word.length > 0) // Remove empty strings
+ .map((word, index) => {
+ // First word should be all lowercase
+ if (index === 0) {
+ return word.toLowerCase();
+ }
+ // Other words should have their first letter capitalized and the rest lowercase
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+ })
+ .join("");
+}
+
+export async function formatPromptWithMentions(
+ prompt: string,
+ charmManager: CharmManager,
+): Promise<
+ {
+ text: string;
+ sources: Record<
+ string,
+ { name: string; cell: Cell; recipe?: Recipe | Module }
+ >;
+ }
+> {
+ const payload = await parseComposerDocument(
+ prompt,
+ charmManager,
+ );
+
+ // Create a mapping of IDs to source objects
+ const sourcesMap: Record = {};
+
+ // Process the text to inject IDs where mentions are
+ let processedText = payload.text;
+
+ // Check if there are any sources to process
+ if (payload.sources && Object.keys(payload.sources).length > 0) {
+ // Add each source to the map
+ Object.entries(payload.sources).forEach(([id, source]) => {
+ const shadowId = getCharmNameAsCamelCase(source.cell, sourcesMap);
+ sourcesMap[shadowId] = source;
+
+ // Replace the markdown link mention with the ID
+ // Format: [character](charm://id)
+ processedText = processedText.replace(
+ new RegExp(`\\[(.*?)\\]\\(charm://${id}\\)`, "g"),
+ `\`${shadowId}\``,
+ );
+ });
+ }
+
+ return {
+ text: processedText,
+ sources: sourcesMap,
+ };
+}
+
+export function getCharmNameAsCamelCase(
+ cell: Cell,
+ usedKeys: Record,
+): string {
+ const charmName = toCamelCase(cell.asSchema(charmSchema).key(NAME).get());
+
+ let name = charmName;
+ let num = 0;
+
+ while (name in usedKeys) name = charmName + `${++num}`;
+
+ return name;
+}
+
+// Type definition for Slate document node structure
+type Descendant = {
+ type?: string;
+ text?: string;
+ id?: string;
+ character?: string;
+ bold?: boolean;
+ italic?: boolean;
+ children?: Descendant[];
+};
+
+// Function to parse Slate document and extract mention references
+export async function parseComposerDocument(
+ serializedDocument: string,
+ charmManager?: CharmManager,
+): Promise<{
+ text: string;
+ mentions: string[];
+ sources: {
+ [id: string]: { name: string; cell: Cell; recipe?: Recipe | Module };
+ };
+}> {
+ try {
+ if (serializedDocument.trim().length == 0) {
+ return { text: "", mentions: [], sources: {} };
+ }
+
+ const document = JSON.parse(serializedDocument) as Descendant[];
+ let fullText = "";
+ const mentions: string[] = [];
+ const sources: {
+ [id: string]: { name: string; cell: Cell };
+ } = {};
+ const mentionIndices: Record = {};
+
+ // Helper to add markdown styling based on node type
+ const processNode = async (
+ node: any,
+ currentList: { type: string | null; level: number } = {
+ type: null,
+ level: 0,
+ },
+ ) => {
+ if (node.type === "mention") {
+ if (node.id) {
+ // Add to mentions list if not already present
+ if (!mentionIndices[node.id]) {
+ mentions.push(node.id);
+
+ // Create bibliography entry if charmManager is provided
+ const bibIndex = Object.keys(sources).length + 1;
+
+ if (charmManager) {
+ const charm = await charmManager.get(node.id);
+ if (charm) {
+ sources[node.id] = {
+ name: node.character || `Reference ${bibIndex}`,
+ cell: charm,
+ };
+
+ mentionIndices[node.id] = bibIndex;
+ }
+ }
+ }
+
+ // Add reference in markdown format
+ fullText += `[${node.character}](charm://${node.id})`;
+ } else {
+ // Handle mentions without explicit IDs (plain text mentions)
+ fullText += `@${node.character}`;
+ }
+ } else if (node.text !== undefined) {
+ // Handle text with formatting
+ let textContent = node.text;
+ if (node.bold) textContent = `**${textContent}**`;
+ if (node.italic) textContent = `*${textContent}*`;
+ fullText += textContent;
+ } else if (node.children) {
+ // Handle block elements with markdown syntax
+ switch (node.type) {
+ case "heading-one":
+ fullText += "# ";
+ break;
+ case "heading-two":
+ fullText += "## ";
+ break;
+ case "heading-three":
+ fullText += "### ";
+ break;
+ case "heading-four":
+ fullText += "#### ";
+ break;
+ case "heading-five":
+ fullText += "##### ";
+ break;
+ case "heading-six":
+ fullText += "###### ";
+ break;
+ case "block-quote":
+ fullText += "> ";
+ break;
+ case "bulleted-list":
+ // Just process children - the list items will add the markers
+ for (const child of node.children) {
+ await processNode(child, {
+ type: "bulleted-list",
+ level: currentList.level + 1,
+ });
+ }
+ return; // Skip the default children processing below
+ case "list-item":
+ fullText += "* ";
+ break;
+ }
+
+ // Process children
+ for (const child of node.children) {
+ await processNode(child, currentList);
+ }
+
+ // Add appropriate line breaks after block elements
+ if (node.type && node.type !== "list-item") {
+ fullText += "\n\n";
+ } else if (node.type === "list-item") {
+ fullText += "\n";
+ }
+ }
+ };
+
+ // Process each node sequentially with await
+ for (const node of document) {
+ await processNode(node);
+ }
+
+ return {
+ text: fullText.trim(), // Remove extra whitespace
+ mentions,
+ sources,
+ };
+ } catch (error) {
+ console.error("Failed to parse document:", error);
+ return { text: "", mentions: [], sources: {} };
+ }
+}
+
+// Helper function to replace mentions with their actual content
+export function replaceMentionsWithContent(
+ parsedDocument: { text: string; mentions: string[] },
+ mentionContent: Record,
+): string {
+ let result = parsedDocument.text;
+
+ // Replace each mention with its content
+ for (const mentionId of parsedDocument.mentions) {
+ const content = mentionContent[mentionId];
+ if (content) {
+ // Find the mention pattern in the text and replace it with content
+ const mentionRegex = new RegExp(`@[^@]+(#${mentionId})\\)`, "g");
+ result = result.replace(mentionRegex, content);
+ }
+ }
+
+ return result;
+}
diff --git a/charm/src/imagine.ts b/charm/src/imagine.ts
new file mode 100644
index 000000000..d46a16839
--- /dev/null
+++ b/charm/src/imagine.ts
@@ -0,0 +1,31 @@
+import { Cell, getEntityId, isCell, isStream } from "@commontools/runner";
+import { isObj } from "@commontools/utils";
+import { JSONSchema } from "@commontools/builder";
+import { Charm, CharmManager } from "./charm.ts";
+import { getIframeRecipe } from "./iframe/recipe.ts";
+import { extractUserCode } from "./iframe/static.ts";
+
+// Re-export workflow types and functions from workflow module
+export type { WorkflowConfig, WorkflowType } from "./workflow.ts";
+
+export { executeWorkflow, WORKFLOWS } from "./workflow.ts";
+
+/**
+ * Structure representing a successfully parsed mention in a prompt
+ */
+export interface ParsedMention {
+ id: string;
+ name: string;
+ originalText: string;
+ startIndex: number;
+ endIndex: number;
+ charm: Cell;
+}
+
+/**
+ * Result of processing a prompt with mentions
+ */
+export interface ProcessedPrompt {
+ text: string; // Processed text with mentions replaced by readable names
+ mentions: Record>; // Map of mention IDs to charm cells
+}
diff --git a/charm/src/index.ts b/charm/src/index.ts
index 7a1c792e2..475278100 100644
--- a/charm/src/index.ts
+++ b/charm/src/index.ts
@@ -10,6 +10,7 @@ export {
compileAndRunRecipe,
compileRecipe,
generateNewRecipeVersion,
+ genSrc,
iterate,
} from "./iterate.ts";
export {
@@ -20,7 +21,34 @@ export {
export {
addGithubRecipe,
castSpellAsCharm,
+ extendCharm,
fixItCharm,
+ modifyCharm,
renameCharm,
} from "./commands.ts";
export { getIframeRecipe, type IFrameRecipe } from "./iframe/recipe.ts";
+export { type ParsedMention, type ProcessedPrompt } from "./imagine.ts";
+export { formatPromptWithMentions, parseComposerDocument } from "./format.ts";
+
+// Export workflow module
+export {
+ classifyIntent,
+ createWorkflowForm,
+ executeEditWorkflow,
+ executeFixWorkflow,
+ executeImagineWorkflow as executeReworkWorkflow,
+ executeWorkflow,
+ type ExecutionPlan,
+ fillClassificationSection,
+ fillPlanningSection,
+ formatSpecWithPlanAndPrompt,
+ generateCode,
+ generatePlan,
+ type IntentClassificationResult,
+ processInputSection,
+ processWorkflow,
+ type WorkflowConfig,
+ type WorkflowForm,
+ WORKFLOWS,
+ type WorkflowType,
+} from "./workflow.ts";
diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts
index 9a9773c90..fd3fd59a2 100644
--- a/charm/src/iterate.ts
+++ b/charm/src/iterate.ts
@@ -5,7 +5,7 @@ import {
registerNewRecipe,
tsToExports,
} from "@commontools/runner";
-import { client as llm } from "@commontools/llm";
+import { client as llm, formatForm } from "@commontools/llm";
import { isObj } from "@commontools/utils";
import {
createJsonSchema,
@@ -18,8 +18,12 @@ import { buildFullRecipe, getIframeRecipe } from "./iframe/recipe.ts";
import { buildPrompt, RESPONSE_PREFILL } from "./iframe/prompt.ts";
import { generateSpecAndSchema } from "@commontools/llm";
import { injectUserCode } from "./iframe/static.ts";
+import { WorkflowForm } from "./index.ts";
-const genSrc = async ({
+/**
+ * Generate source code for a charm based on its specification, schema, and optional existing source
+ */
+export const genSrc = async ({
src,
spec,
newSpec,
@@ -47,11 +51,14 @@ const genSrc = async ({
return source;
};
+/**
+ * Iterate on an existing charm by generating new source code based on a new specification
+ * This is a core function used by various workflows
+ */
export async function iterate(
charmManager: CharmManager,
charm: Cell,
- spec: string,
- shiftKey: boolean,
+ plan: WorkflowForm["plan"],
model?: string,
): Promise> {
const { iframe } = getIframeRecipe(charm);
@@ -59,11 +66,13 @@ export async function iterate(
throw new Error("Cannot iterate on a non-iframe. Must extend instead.");
}
- const newSpec = shiftKey ? iframe.spec + "\n" + spec : spec;
+ // TODO(bf): questionable logic...
+ const iframeSpec = iframe.spec;
+ const newSpec = plan?.spec ?? iframeSpec;
const newIFrameSrc = await genSrc({
src: iframe.src,
- spec: iframe.spec,
+ spec: iframeSpec,
newSpec,
schema: iframe.argumentSchema,
model: model,
@@ -196,13 +205,12 @@ function turnCellsIntoAliases(data: any): any {
*/
export async function castNewRecipe(
charmManager: CharmManager,
- goal: string,
- cells?: any,
+ form: WorkflowForm,
): Promise> {
- console.log("Processing goal:", goal, cells);
+ console.log("Processing form:", form);
// Remove $UI, $NAME, and any streams from the cells
- const scrubbed = scrub(cells);
+ const scrubbed = scrub(form.input.references);
// First, extract any existing schema if we have data
const existingSchema = createJsonSchema(scrubbed);
@@ -214,12 +222,14 @@ export async function castNewRecipe(
title,
description,
plan,
- } = await generateSpecAndSchema(goal, existingSchema);
+ } = await generateSpecAndSchema(form, existingSchema);
console.log("resultSchema", resultSchema);
- const newSpec =
- `${goal}\n${plan}\n${spec}`;
+ // We're goig from loose plan to detailed plan here.
+ const newSpec = `${
+ formatForm(form)
+ }\n${plan}\n${spec}`;
console.log("newSpec", newSpec);
@@ -227,7 +237,7 @@ export async function castNewRecipe(
// as a hack to work around iframes not supporting results schemas
const schema = {
...existingSchema,
- title,
+ title: title || "missing",
description,
} as Writable;
@@ -259,7 +269,7 @@ export async function castNewRecipe(
src: newIFrameSrc,
spec,
plan,
- goal,
+ goal: form.input.processedInput,
argumentSchema: schema,
resultSchema,
name,
diff --git a/charm/src/workflow.ts b/charm/src/workflow.ts
new file mode 100644
index 000000000..6059a0afe
--- /dev/null
+++ b/charm/src/workflow.ts
@@ -0,0 +1,899 @@
+/**
+ * Workflow module - Contains the core workflow processing pipeline for charm operations
+ *
+ * This module defines:
+ * 1. Workflow types (Fix, Edit, Rework)
+ * 2. Classification process
+ * 3. Plan generation pipeline
+ * 4. Schema and specification generation
+ * 5. Workflow steps and execution
+ */
+
+import { Cell } from "@commontools/runner";
+import { Charm, CharmManager } from "./charm.ts";
+import { JSONSchema } from "@commontools/builder";
+import { classifyWorkflow, generateWorkflowPlan } from "@commontools/llm";
+import { genSrc, iterate } from "./iterate.ts";
+import { getIframeRecipe } from "./iframe/recipe.ts";
+import { extractUserCode } from "./iframe/static.ts";
+import { formatPromptWithMentions } from "./format.ts";
+import { castNewRecipe } from "./iterate.ts";
+
+// Types for workflow classification
+export type WorkflowType = "fix" | "edit" | "imagine";
+
+// Configuration for each workflow type
+export interface WorkflowConfig {
+ name: WorkflowType;
+ description: string;
+ updateSpec: boolean;
+ updateSchema: boolean;
+ allowsDataReferences: boolean;
+}
+
+export const WORKFLOWS: Record = {
+ fix: {
+ name: "fix",
+ description:
+ "Fix issues in the code without changing functionality or spec",
+ updateSpec: false,
+ updateSchema: false,
+ allowsDataReferences: false,
+ },
+ edit: {
+ name: "edit",
+ description:
+ "Update functionality while maintaining the same core data structure",
+ updateSpec: true,
+ updateSchema: false,
+ allowsDataReferences: false,
+ },
+ imagine: {
+ name: "imagine",
+ description: "Create a new charm with a potentially different data schema",
+ updateSpec: true,
+ updateSchema: true,
+ allowsDataReferences: true,
+ },
+};
+
+/**
+ * Results from the intent classification stage
+ */
+export interface IntentClassificationResult {
+ workflowType: WorkflowType;
+ confidence: number; // 0-1 confidence score
+ reasoning: string;
+ enhancedPrompt?: string; // Optional refined prompt based on classification
+}
+
+/**
+ * The execution plan for a workflow
+ */
+export interface ExecutionPlan {
+ workflowType: WorkflowType;
+ steps: string[];
+ spec: string;
+ dataModel: string;
+}
+
+/**
+ * Structure representing a successfully parsed mention in a prompt
+ */
+export interface ParsedMention {
+ id: string;
+ name: string;
+ originalText: string;
+ startIndex: number;
+ endIndex: number;
+ charm: Cell;
+}
+
+/**
+ * Result of processing a prompt with mentions
+ */
+export interface ProcessedPrompt {
+ text: string; // Processed text with mentions replaced by readable names
+ mentions: Record>; // Map of mention IDs to charm cells
+}
+
+/**
+ * Step 1: Classify user intent into one of the supported workflows
+ *
+ * This is the first step in the workflow pipeline. It processes the user
+ * input and determines which workflow type best matches their intent.
+ *
+ * @param input User input text
+ * @param currentCharm Current charm context (optional)
+ * @param model LLM model to use
+ * @param references Referenced charm data
+ * @returns Classification result
+ */
+export async function classifyIntent(
+ input: string,
+ currentCharm?: Cell,
+ model?: string,
+ references?: Record>,
+): Promise {
+ // Process the input for @mentions if a CharmManager is provided
+ // Extract context from the current charm if available
+ let existingSpec: string | undefined;
+ let existingSchema: JSONSchema | undefined;
+ let existingCode: string | undefined;
+
+ if (currentCharm) {
+ const { spec, schema, code } = extractContext(currentCharm);
+ existingSpec = spec;
+ existingSchema = schema;
+ existingCode = code;
+ }
+
+ if (!existingSpec || !existingSchema) {
+ return {
+ workflowType: "imagine",
+ confidence: 1.0,
+ reasoning:
+ "Automatically classified as 'imagine' because there is nothing to refer to (either no current charm or no iframe recipe).",
+ enhancedPrompt: input,
+ };
+ }
+
+ // Check if we have any mentions of other charms (except the current charm)
+ // If so, we should automatically classify as "rework" since we need to build
+ // a combined schema and create a new argument cell that can access all referenced data
+ const hasOtherCharmReferences = references &&
+ Object.keys(references).filter((key) => key !== "currentCharm")
+ .length > 0;
+
+ if (hasOtherCharmReferences) {
+ // Auto-classify as rework when referencing other charms
+ return {
+ workflowType: "imagine",
+ confidence: 1.0,
+ reasoning:
+ "Automatically classified as 'imagine' because the prompt references other charms. " +
+ "When referencing other charms, we need to construct a new argument cell that can " +
+ "access data from all references with a combined schema.",
+ enhancedPrompt: input,
+ };
+ }
+
+ try {
+ const result = await classifyWorkflow(
+ input,
+ existingSpec,
+ existingSchema,
+ existingCode,
+ model,
+ );
+
+ return {
+ workflowType: result.workflowType,
+ confidence: result.confidence,
+ reasoning: result.reasoning,
+ enhancedPrompt: result.enhancedPrompt,
+ };
+ } catch (error) {
+ console.error("Error during workflow classification:", error);
+
+ // If no references, fallback to a simple heuristic based on keywords
+ const lowerInput = input.toLowerCase();
+
+ if (
+ lowerInput.includes("fix") || lowerInput.includes("bug") ||
+ lowerInput.includes("issue")
+ ) {
+ return {
+ workflowType: "fix",
+ confidence: 0.7,
+ reasoning: "Fallback classification: Input suggests a fix operation",
+ };
+ } else if (
+ lowerInput.includes("edit") || lowerInput.includes("update") ||
+ lowerInput.includes("improve") || lowerInput.includes("add")
+ ) {
+ return {
+ workflowType: "edit",
+ confidence: 0.6,
+ reasoning:
+ "Fallback classification: Input suggests enhancing functionality",
+ };
+ } else {
+ return {
+ workflowType: "imagine",
+ confidence: 0.5,
+ reasoning: "Fallback classification: Input suggests new functionality",
+ };
+ }
+ }
+}
+
+function extractContext(charm: Cell) {
+ let spec: string | undefined;
+ let schema: JSONSchema | undefined;
+ let code: string | undefined;
+
+ try {
+ const iframeRecipe = getIframeRecipe(charm);
+ if (iframeRecipe && iframeRecipe.iframe) {
+ spec = iframeRecipe.iframe.spec;
+ schema = iframeRecipe.iframe.argumentSchema;
+ code = extractUserCode(iframeRecipe.iframe.src || "") ||
+ undefined;
+ }
+ } catch {
+ console.warn("Failed to extract context from charm");
+ }
+
+ return {
+ spec,
+ schema,
+ code,
+ };
+}
+
+/**
+ * Step 2: Generate execution plan for the given intent and workflow
+ *
+ * This is the second step in the workflow pipeline. Based on the classified
+ * workflow type, it generates an execution plan, updated spec, and schema.
+ *
+ * @param input User input
+ * @param workflowType The classified workflow type
+ * @param currentCharm Current charm context
+ * @param model LLM model to use
+ * @param references Referenced charm data
+ * @returns Execution plan with steps, spec, and schema
+ */
+export async function generatePlan(
+ { input, workflowType, currentCharm, model }: {
+ input: string;
+ workflowType: WorkflowType;
+ currentCharm?: Cell;
+ model?: string;
+ },
+): Promise {
+ // Extract context from the current charm if available
+ let existingSpec: string | undefined;
+ let existingSchema: JSONSchema | undefined;
+ let existingCode: string | undefined;
+
+ if (currentCharm) {
+ const { spec, schema, code } = extractContext(currentCharm);
+ existingSpec = spec;
+ existingSchema = schema;
+ existingCode = code;
+ }
+
+ try {
+ const result = await generateWorkflowPlan(
+ input,
+ workflowType,
+ existingSpec,
+ existingSchema,
+ existingCode,
+ model,
+ );
+
+ return {
+ workflowType,
+ steps: result.steps,
+ spec: result.spec,
+ dataModel: result.dataModel,
+ };
+ } catch (error) {
+ console.error("Error during plan generation:", error);
+
+ // Fallback to a simple plan if LLM generation fails
+ const steps: string[] = [];
+
+ if (workflowType === "fix") {
+ steps.push("Analyze existing code to identify the issue");
+ steps.push("Implement fix while maintaining current functionality");
+ steps.push("Verify the fix doesn't introduce side effects");
+ } else if (workflowType === "edit") {
+ steps.push("Update specification to reflect new requirements");
+ steps.push("Modify code to implement the new functionality");
+ steps.push("Ensure backward compatibility with existing data");
+ } else { // imagine
+ steps.push("Generate new specification and schema");
+ steps.push("Create new implementation based on requirements");
+ steps.push("Link to referenced data from existing charms");
+ }
+
+ return {
+ workflowType,
+ steps,
+ spec: input,
+ dataModel: "",
+ };
+ }
+}
+
+/**
+ * The workflow form that contains all the data needed for code generation
+ * This is progressively filled in through the workflow process
+ */
+export interface WorkflowForm {
+ // Input information
+ input: {
+ rawInput: string;
+ processedInput: string;
+ existingCharm?: Cell;
+ references: Record>;
+ };
+
+ // Classification information
+ classification: {
+ workflowType: WorkflowType;
+ confidence: number;
+ reasoning: string;
+ } | null;
+
+ // Planning information
+ plan: {
+ steps: string[];
+ spec?: string;
+ dataModel?: string;
+ } | null;
+
+ // Generation information (only when actually generating code)
+ generation?: {
+ charm: Cell;
+ };
+
+ // Metadata and workflow state
+ meta: {
+ isComplete: boolean;
+ isFilled: boolean;
+ modelId?: string;
+ charmManager?: CharmManager;
+ };
+}
+
+/**
+ * Create a new workflow form with default values
+ */
+export function createWorkflowForm(
+ { input, modelId, charm }: {
+ input: string;
+ modelId?: string;
+ charm?: Cell;
+ },
+): WorkflowForm {
+ return {
+ input: {
+ rawInput: input,
+ processedInput: "",
+ references: {},
+ existingCharm: charm,
+ },
+ classification: null,
+ plan: null,
+ meta: {
+ isComplete: false,
+ isFilled: false,
+ modelId,
+ },
+ };
+}
+
+/**
+ * Process the input part of the workflow form
+ * Handles mentions, references, and sets up the processedInput
+ */
+export async function processInputSection(
+ charmManager: CharmManager,
+ form: WorkflowForm,
+ options: Record = {},
+): Promise {
+ const newForm = { ...form };
+
+ // Skip for empty inputs
+ if (!form.input.rawInput || form.input.rawInput.trim().length === 0) {
+ throw new Error("Input is empty");
+ }
+
+ // Process mentions if CharmManager is provided
+ let processedInput = form.input.rawInput;
+ const references = { ...form.input.references };
+
+ try {
+ const { text, sources } = await formatPromptWithMentions(
+ form.input.rawInput,
+ charmManager,
+ );
+ processedInput = text;
+
+ // Merge mentioned charms into references
+ for (const [mentionId, charm] of Object.entries(sources)) {
+ if (!references[mentionId]) {
+ references[mentionId] = charm.cell;
+ }
+ }
+ } catch (error) {
+ console.warn("Error processing mentions in form:", error);
+ }
+
+ newForm.input.processedInput = processedInput;
+ newForm.input.references = references;
+ newForm.meta.charmManager = charmManager;
+
+ return newForm;
+}
+
+/**
+ * Fill the classification section of the workflow form
+ */
+export async function fillClassificationSection(
+ form: WorkflowForm,
+ options: { model?: string } = {},
+): Promise {
+ const newForm = { ...form };
+
+ // Skip for empty inputs
+ if (!form.input.rawInput || form.input.rawInput.trim().length === 0) {
+ newForm.classification = {
+ workflowType: "edit",
+ confidence: 0,
+ reasoning: "Empty input",
+ };
+ return newForm;
+ }
+
+ const classification = await classifyIntent(
+ form.input.processedInput,
+ form.input.existingCharm,
+ form.meta.modelId,
+ form.input.references,
+ );
+
+ // Update classification in the form
+ newForm.classification = {
+ workflowType: classification.workflowType as WorkflowType,
+ confidence: classification.confidence,
+ reasoning: classification.reasoning,
+ };
+
+ return newForm;
+}
+
+/**
+ * Fill the planning section of the workflow form
+ */
+export async function fillPlanningSection(
+ form: WorkflowForm,
+ options: Record = {},
+): Promise {
+ if (!form.classification) {
+ throw new Error("Classification is required");
+ }
+
+ const newForm = { ...form };
+
+ // Skip for empty inputs
+ if (!form.input.rawInput || form.input.rawInput.trim().length === 0) {
+ newForm.plan = {
+ steps: [],
+ };
+ return newForm;
+ }
+
+ let planningResult;
+ // Generate new plan based on workflow type
+ if (
+ form.classification.workflowType === "fix" && form.input.existingCharm
+ ) {
+ // For fix workflow, preserve existing spec
+ let existingSpec: string | undefined;
+ let existingSchema: JSONSchema | undefined;
+ let existingCode: string | undefined;
+
+ if (form.input.existingCharm) {
+ const { spec, schema, code } = extractContext(form.input.existingCharm);
+ existingSpec = spec;
+ existingSchema = schema;
+ existingCode = code;
+ }
+
+ // Generate just the plan without updating spec
+ const executionPlan = await generatePlan(
+ {
+ input: form.input.processedInput,
+ workflowType: form.classification.workflowType,
+ currentCharm: form.input.existingCharm,
+ model: form.meta.modelId,
+ },
+ );
+
+ planningResult = {
+ steps: executionPlan.steps,
+ spec: existingSpec, // Use existing spec for fix workflow
+ dataModel: "",
+ };
+ } else {
+ // For edit/imagine, generate both plan and spec
+ const executionPlan = await generatePlan(
+ {
+ input: form.input.processedInput,
+ workflowType: form.classification.workflowType,
+ currentCharm: form.input.existingCharm,
+ model: form.meta.modelId,
+ },
+ );
+
+ planningResult = {
+ steps: executionPlan.steps,
+ spec: executionPlan.spec,
+ dataModel: executionPlan.dataModel,
+ };
+ }
+
+ // Update planning in the form
+ newForm.plan = {
+ steps: planningResult.steps || [],
+ spec: planningResult.spec,
+ dataModel: planningResult.dataModel,
+ };
+
+ // Mark the form as filled (ready for generation) once we have a plan
+ newForm.meta.isFilled = true;
+
+ return newForm;
+}
+
+/**
+ * Generate code based on a filled workflow form
+ * This is the final step that actually creates a charm
+ */
+export async function generateCode(form: WorkflowForm): Promise {
+ if (!form.classification || !form.plan) {
+ throw new Error("Classification and plan are required for code generation");
+ }
+
+ const newForm = { ...form };
+
+ // Check if the form is filled properly
+ if (!newForm.meta.isFilled) {
+ throw new Error("Cannot generate code from an incomplete workflow form");
+ }
+
+ if (!newForm.meta.charmManager) {
+ throw new Error("CharmManager is required for code generation");
+ }
+
+ let charm: Cell;
+
+ // Execute the appropriate workflow based on the classification
+ switch (form.classification.workflowType) {
+ case "fix":
+ if (!form.input.existingCharm) {
+ throw new Error("Fix workflow requires an existing charm");
+ }
+ charm = await executeFixWorkflow(
+ newForm.meta.charmManager,
+ form.input.existingCharm,
+ form.plan,
+ form.meta.modelId,
+ );
+ break;
+
+ case "edit":
+ if (!form.input.existingCharm) {
+ throw new Error("Edit workflow requires an existing charm");
+ }
+ charm = await executeEditWorkflow(
+ newForm.meta.charmManager,
+ form.input.existingCharm,
+ form.plan,
+ form.meta.modelId,
+ );
+ break;
+
+ case "imagine":
+ charm = await executeImagineWorkflow(
+ newForm.meta.charmManager,
+ form,
+ );
+ break;
+
+ default:
+ throw new Error(
+ `Unknown workflow type: ${form.classification.workflowType}`,
+ );
+ }
+
+ // Update the form with the generated charm
+ newForm.generation = {
+ charm,
+ };
+
+ // Mark the form as complete
+ newForm.meta.isComplete = true;
+
+ return newForm;
+}
+
+/**
+ * Process a workflow request from start to finish or just fill the form
+ *
+ * @param input The user's input text
+ * @param dryRun If true, only fills the form but doesn't generate code
+ * @param options Additional options for processing
+ * @returns The filled (and possibly completed) workflow form
+ */
+export async function processWorkflow(
+ input: string,
+ dryRun: boolean = false,
+ options: {
+ charmManager?: CharmManager;
+ existingCharm?: Cell;
+ prefill?: Partial;
+ model?: string;
+ } = {},
+): Promise {
+ // Create a new form or use prefilled form
+ let form = createWorkflowForm({
+ input,
+ charm: options.existingCharm,
+ modelId: options.model,
+ });
+
+ if (options.prefill) {
+ form = { ...form, ...options.prefill };
+ }
+
+ // Step 1: Process input (mentions, references, etc.) if not already processed
+ if (
+ !form.input?.processedInput ||
+ form.input?.processedInput === form.input?.rawInput
+ ) {
+ if (!options.charmManager) {
+ throw new Error("charmManager required to format input");
+ }
+
+ form = await processInputSection(options.charmManager, form);
+ }
+
+ // Step 2: Classification if not already classified
+ if (!form.classification) {
+ form = await fillClassificationSection(form, {
+ model: options.model,
+ });
+ }
+
+ // Step 3: Planning if not already planned
+ if (!form.plan) {
+ form = await fillPlanningSection(form);
+ }
+
+ // Step 4: Generation (if not a dry run and not already generated)
+ if (!dryRun && options.charmManager && !form.generation?.charm) {
+ form = await generateCode(form);
+ }
+
+ return form;
+}
+
+/**
+ * Format a spec to include user prompt and execution plan
+ * This ensures the full context is preserved in the recipe
+ */
+export function formatSpecWithPlanAndPrompt(
+ originalSpec: string,
+ userPrompt: string,
+ plan: string[] | string,
+): string {
+ // Format the plan as a string if it's an array
+ const planText = Array.isArray(plan) ? plan.join("\n- ") : plan;
+
+ // Create a formatted spec with XML tags to separate sections
+ return `
+${originalSpec}
+
+
+
+${userPrompt}
+
+
+
+- ${planText}
+`;
+}
+
+/**
+ * Execute the Fix workflow
+ *
+ * The Fix workflow preserves the existing specification and schema,
+ * focusing only on fixing issues in the implementation.
+ */
+export function executeFixWorkflow(
+ charmManager: CharmManager,
+ currentCharm: Cell,
+ plan: WorkflowForm["plan"],
+ model?: string,
+): Promise> {
+ console.log("Executing FIX workflow");
+
+ // Call iterate with the existing spec to ensure it's preserved
+ return iterate(
+ charmManager,
+ currentCharm,
+ plan,
+ model,
+ );
+}
+
+/**
+ * Execute the Edit workflow
+ *
+ * The Edit workflow builds upon the existing specification but retains
+ * the existing schema, modifying the implementation to add features
+ * or enhance functionality while maintaining compatibility.
+ */
+export function executeEditWorkflow(
+ charmManager: CharmManager,
+ currentCharm: Cell,
+ plan: WorkflowForm["plan"],
+ model?: string,
+): Promise> {
+ console.log("Executing EDIT workflow");
+
+ // For edit workflow, we use the updated spec but pass shiftKey as true
+ // to ensure it preserves compatibility with existing data
+ return iterate(
+ charmManager,
+ currentCharm,
+ plan,
+ model,
+ );
+}
+
+/**
+ * Helper function to convert a string to camelCase
+ */
+function toCamelCase(input: string): string {
+ // Handle empty string case
+ if (!input) return "currentCharm";
+
+ // Split the input string by non-alphanumeric characters
+ return input
+ .split(/[^a-zA-Z0-9]/)
+ .filter((word) => word.length > 0) // Remove empty strings
+ .map((word, index) => {
+ // First word should be all lowercase
+ if (index === 0) {
+ return word.toLowerCase();
+ }
+ // Other words should have their first letter capitalized and the rest lowercase
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+ })
+ .join("");
+}
+
+/**
+ * Execute the Rework workflow
+ *
+ * The Rework workflow creates a new charm with potentially different
+ * schema, allowing for more significant changes or combinations of
+ * data from multiple existing charms.
+ */
+export function executeImagineWorkflow(
+ charmManager: CharmManager,
+ form: WorkflowForm,
+): Promise> {
+ console.log("Executing IMAGINE workflow");
+
+ // Process references - this allows the new charm to access data from multiple sources
+ let allReferences: Record> = {};
+
+ // Add all external references first with validation
+ if (form.input.references && Object.keys(form.input.references).length > 0) {
+ for (const [id, cell] of Object.entries(form.input.references)) {
+ if (
+ !cell || typeof cell !== "object" || !("get" in cell) ||
+ typeof cell.get !== "function"
+ ) {
+ console.warn(`Reference "${id}" is not a valid cell, skipping`);
+ continue;
+ }
+
+ try {
+ // Create a valid camelCase identifier
+ const cellData = cell.get();
+ const charmName = cellData && cellData["NAME"] ? cellData["NAME"] : id;
+ const camelCaseId = toCamelCase(charmName);
+
+ // Make sure the ID is unique
+ let uniqueId = camelCaseId;
+ let counter = 1;
+ while (uniqueId in allReferences) {
+ uniqueId = `${camelCaseId}${counter++}`;
+ }
+
+ allReferences[uniqueId] = cell;
+ console.log(`Added reference "${id}" as "${uniqueId}"`);
+ } catch (error) {
+ console.error(`Error processing reference "${id}":`, error);
+ }
+ }
+ }
+
+ // Add current charm if available
+ if (
+ form.input.existingCharm
+ ) {
+ try {
+ const charmData = form.input.existingCharm.get();
+ const charmName = charmData && charmData["NAME"]
+ ? charmData["NAME"]
+ : "currentCharm";
+ const camelCaseId = toCamelCase(charmName);
+
+ // Make sure the ID is unique
+ let uniqueId = camelCaseId;
+ let counter = 1;
+ while (uniqueId in allReferences) {
+ uniqueId = `${camelCaseId}${counter++}`;
+ }
+
+ // HACK: avoid nesting for a single self reference
+ if (Object.keys(allReferences).length === 0) {
+ allReferences = form.input.existingCharm as any;
+ } else {
+ allReferences[uniqueId] = form.input.existingCharm;
+ }
+
+ console.log(`Added current charm as "${uniqueId}"`);
+
+ // Remove any generic "currentCharm" entry
+ if (allReferences["currentCharm"]) {
+ delete allReferences["currentCharm"];
+ }
+ } catch (error) {
+ console.error(`Error processing current charm:`, error);
+ }
+ }
+
+ form.input.references = allReferences;
+
+ // Cast a new recipe with references, spec, and schema
+ return castNewRecipe(
+ charmManager,
+ form,
+ );
+}
+
+/**
+ * Main entry point for all workflow processing
+ *
+ * This function orchestrates the entire workflow process:
+ * 1. Input processing and mention detection
+ * 2. Classification (if not overridden)
+ * 3. Plan generation
+ * 4. Code generation
+ */
+export async function executeWorkflow(
+ charmManager: CharmManager,
+ input: string,
+ context: {
+ currentCharm?: Cell;
+ prefill?: Partial;
+ model?: string;
+ },
+): Promise| > {
+ // Process the workflow with dryRun=false to fully execute and generate code
+ const form = await processWorkflow(input, false, {
+ charmManager,
+ existingCharm: context.currentCharm,
+ model: context.model,
+ prefill: context.prefill,
+ });
+
+ // A completed form should have a generated charm
+ if (!form.generation?.charm) {
+ throw new Error("Workflow execution failed to create a charm");
+ }
+
+ return form.generation.charm;
+}
diff --git a/charm/test/iterate.test.ts b/charm/test/iterate.test.ts
index 8309f2876..36fb45291 100644
--- a/charm/test/iterate.test.ts
+++ b/charm/test/iterate.test.ts
@@ -1,9 +1,10 @@
-import { assertEquals } from "@std/assert";
-import { beforeEach, describe, it } from "@std/testing/bdd";
+import { assertEquals, assertExists } from "@std/assert";
+import { beforeEach, describe, it, afterEach } from "@std/testing/bdd";
import { scrub } from "../src/iterate.ts";
-import { getImmutableCell, isCell, isStream } from "@commontools/runner";
+import { getImmutableCell, isCell, isStream, Cell } from "@commontools/runner";
import { JSONSchema } from "@commontools/builder";
import { isObj } from "@commontools/utils";
+import { Charm, CharmManager } from "../src/charm.ts";
describe("scrub function", () => {
it("should return primitive values unchanged", () => {
@@ -86,3 +87,6 @@ describe("scrub function", () => {
assertEquals(result.get(), "test value");
});
});
+
+// XML tag functions have been moved elsewhere
+// Tests will be updated when we create proper tests for the workflow module
diff --git a/charm/test/workflow-classification.test.ts b/charm/test/workflow-classification.test.ts
new file mode 100644
index 000000000..4ac58e4e0
--- /dev/null
+++ b/charm/test/workflow-classification.test.ts
@@ -0,0 +1,35 @@
+import { describe, it } from "@std/testing/bdd";
+import { expect } from "@std/expect";
+
+import { classifyIntent, generatePlan, WORKFLOWS } from "../src/workflow.ts";
+
+describe("Workflow Classification", () => {
+ describe("WORKFLOWS constant", () => {
+ it("should define three workflow types", () => {
+ expect(Object.keys(WORKFLOWS).length).toBe(3);
+ expect(WORKFLOWS.fix).toBeDefined();
+ expect(WORKFLOWS.edit).toBeDefined();
+ expect(WORKFLOWS.imagine).toBeDefined();
+ });
+
+ it("should have the correct properties for each workflow", () => {
+ // Fix workflow
+ expect(WORKFLOWS.fix.name).toBe("fix");
+ expect(WORKFLOWS.fix.updateSpec).toBe(false);
+ expect(WORKFLOWS.fix.updateSchema).toBe(false);
+ expect(WORKFLOWS.fix.allowsDataReferences).toBe(false);
+
+ // Edit workflow
+ expect(WORKFLOWS.edit.name).toBe("edit");
+ expect(WORKFLOWS.edit.updateSpec).toBe(true);
+ expect(WORKFLOWS.edit.updateSchema).toBe(false);
+ expect(WORKFLOWS.edit.allowsDataReferences).toBe(false);
+
+ // Rework workflow
+ expect(WORKFLOWS.imagine.name).toBe("imagine");
+ expect(WORKFLOWS.imagine.updateSpec).toBe(true);
+ expect(WORKFLOWS.imagine.updateSchema).toBe(true);
+ expect(WORKFLOWS.imagine.allowsDataReferences).toBe(true);
+ });
+ });
+});
diff --git a/jumble/integration/basic-flow.test.ts b/jumble/integration/basic-flow.test.ts
index 66f2ca8e2..12e3444c4 100644
--- a/jumble/integration/basic-flow.test.ts
+++ b/jumble/integration/basic-flow.test.ts
@@ -212,12 +212,12 @@ Deno.test({
await page.keyboard.up("ControlLeft");
await sleep(1000);
- await page.keyboard.type("extend");
+ await page.keyboard.type("modify");
await sleep(1000);
await page.keyboard.press("Enter");
- await sleep(300);
- await page.keyboard.type("count of values");
+ await sleep(500);
+ await page.keyboard.type("count the values in current");
await sleep(1000);
await page.keyboard.press("Enter");
await sleep(1000);
@@ -234,7 +234,7 @@ Deno.test({
await waitForSelectorWithText(
page,
"a[aria-roledescription='charm-link']",
- "Value Counter",
+ "Value Counter Charm",
);
// FIXME(ja): how to look at the actual iframe content?
diff --git a/jumble/integration/cache/llm-api-cache/61063134e61d3f147733a0cf9711b6fa9cf9f2d5acf81bfeccf71285c0ed93f9.json b/jumble/integration/cache/llm-api-cache/61063134e61d3f147733a0cf9711b6fa9cf9f2d5acf81bfeccf71285c0ed93f9.json
deleted file mode 100644
index d22b0d3c4..000000000
--- a/jumble/integration/cache/llm-api-cache/61063134e61d3f147733a0cf9711b6fa9cf9f2d5acf81bfeccf71285c0ed93f9.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "model": "anthropic:claude-3-7-sonnet-latest",
- "system": "# React Component Builder\n\nCreate an interactive React component that fulfills the user's request. Focus on delivering a clean, useful implementation with appropriate features.\n\n## You Are Part of a Two-Phase Process\n\n1. First phase (already completed):\n - Analyzed the user's request\n - Created a detailed specification\n - Generated a structured data schema\n\n2. Your job (second phase):\n - Create a reactive UI component based on the provided specification and schema\n - Implement the UI exactly according to the specification\n - Strictly adhere to the data schema provided\n\n## Required Elements\n- Define a title with `const title = 'Your App Name';`\n- Implement both `onLoad` and `onReady` functions\n- Use Tailwind CSS for styling with tasteful defaults\n- Do not write | | | | | | | |