Skip to content

fix(copilot): sanitizeForCopilot exposes raw UUID block keys to LLM #3628

@minijeong-log

Description

@minijeong-log

Bug Description

sanitizeForCopilot in lib/workflows/sanitization/json-sanitizer.ts uses internal UUID block IDs as keys when serializing workflow state for the LLM. This means the copilot model sees output like:

{
  "blocks": {
    "087b3e77-xxxx-xxxx-xxxx-xxxxxxxxxxxx": {
      "type": "agent",
      "name": "My Agent",
      "inputs": { "prompt": "..." },
      "connections": {
        "source": "a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
    }
  }
}

When the model then generates edit_workflow operations referencing these blocks, it produces UUID-based references like <087b3e77-xxxx.output>, which are fragile and hard to interpret.

Why This Is a Problem

  • Smaller / open-weight models (e.g. those served via vLLM or Ollama) tend to copy UUID keys verbatim into generated references, resulting in broken workflows.
  • Even high-capability models (Claude Sonnet/Opus) would benefit from human-readable keys — it reduces token waste and makes copilot interactions more predictable.

Expected Behavior

sanitizeForCopilot should replace UUID block keys with deterministic, human-readable identifiers derived from the block type and name, for example:

{
  "blocks": {
    "start": { "type": "starter", "name": "Start", ... },
    "agent_1": { "type": "agent", "name": "My Agent", ... }
  }
}

Connection targets should also reference the human-readable keys instead of UUIDs.

Analysis

Changing sanitizeForCopilot alone is not sufficient. The fix requires changes across multiple layers:

1. Sanitization layer (json-sanitizer.ts)

  • Build a UUID to readable key mapping (e.g. {type} for unique types, {type}_{index} for duplicates)
  • Use readable keys for block output, connection targets, and nested nodes

2. Operation normalization layer (new)

A reverse mapping step is needed before the engine processes copilot-generated operations:

  • Resolve name-based block_id to UUID for edit/delete operations
  • Resolve name-based connection targets to UUID
  • Replace name-based <block.field> input references to UUID-based references
  • Handle edge case: model adding a starter block when start_trigger already exists — convert to edit

3. Engine layer (edit-workflow/engine.ts, builders.ts)

  • Integrate the normalizer before operation execution

Why all layers are needed

Without the reverse mapping, the copilot generates operations with readable keys (e.g. block_id: "agent_1") that the engine cannot resolve — it expects UUIDs. This causes all edit/delete operations on existing blocks to fail silently.

Proposed Approach

Given the scope of changes required, I'd like to discuss the preferred approach before submitting a PR:

  1. Centralizing normalization — A single normalizer file that handles all name-UUID resolution, keeping engine.ts and builders.ts changes minimal
  2. Backwards compatibility — The normalizer should fall through to UUID if the block_id is already a valid UUID, so existing behavior is preserved
  3. Testing strategy — Unit tests for the key mapping, reverse mapping, and end-to-end operation normalization

Would appreciate feedback on the approach before proceeding with implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions