Assistant Mode: Dual-Mode Operation

Human edits and AI workflows share one Design Graph through a unified event pipeline

Table of Contents

  1. The Two Runtime Modes
  2. Change Detection Layer
    1. Detection Pipeline
    2. Adapter Interface
  3. Human-Edit-Triggered Workflow
    1. Key Difference: Validation Timing
    2. Ingest Failure Handling
  4. Bidirectional Artifact Synchronization (Drift Detection)
    1. Drift Direction 1: File Newer Than Graph (Human Edit)
    2. Drift Direction 2: Graph Newer Than File (Agent Mutation)
    3. Periodic Reconciler
  5. New Event Types
    1. EventType Additions
    2. Kafka Topic Additions
  6. Relationship to ADR-001
    1. Ingest Activity Definition
  7. Related Documents

The Two Runtime Modes

MetaForge supports two concurrent runtime modes. Both write to the same Design Graph through the same event pipeline — the graph is the source of truth, not the agent, not the human.

Aspect Assistant Mode Autonomous Mode
Trigger Engineer saves a file in KiCad, FreeCAD, or VS Code Orchestrator dispatches a task to a Domain Agent
Actor actor_id = "user:<id>" actor_id = "agent:<code>"
Change origin File-world (tool-specific format) Semantic-world (graph mutation)
Validation timing After apply — change is trusted, validation reports issues Before commit — evaluate_change() can reject the proposal
Approval flow Implicit (engineer saved intentionally) Explicit (IDE Assistant shows diff, engineer approves)
Constraint violations Surfaced as warnings via IDE Assistant notification Block the mutation until resolved
Typical latency < 2 seconds (file save → notification) Seconds to minutes (LLM reasoning + tool execution)

These are runtime states, not product roadmap phases. Both modes become available during P1 once the ingest pipeline and IDE Assistants are operational. The three-phase product roadmap (P1 MVP → P2 Operational Twin → P3+ Live Twin) described in Architecture Overview is orthogonal.


Change Detection Layer

When an engineer saves a design file, the system detects the change, parses it into graph events, and ingests it into the event pipeline.

flowchart LR
    ENG["Engineer<br/>KiCad / FreeCAD / VS Code"] -->|"file save"| WF["watchfiles<br/>inotify / FSEvents"]
    WF -->|"raw event"| DEB["Debounce<br/>500ms window"]
    DEB -->|"stable path"| AP["Adapter Parser<br/>KiCad → GraphEvents<br/>FreeCAD → GraphEvents<br/>VS Code → GraphEvents"]
    AP -->|"GraphEvent[]<br/>actor_id = user:id"| ING["Ingest Pipeline"]
    ING -->|"publish"| KAFKA["Kafka<br/>artifact.ingest"]

    style ENG fill:#E67E22,color:#fff
    style KAFKA fill:#2C3E50,color:#fff

Detection Pipeline

  1. watchfiles watcher — Monitors the project directory for file changes using OS-native file system events (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows). Filters to known artifact extensions (.kicad_sch, .kicad_pcb, .FCStd, .step, .py, .c, .h).

  2. Debounce window — Many tools perform multiple rapid writes during a single save operation (temp file → rename, or incremental writes). The debounce layer collects changes within a 500ms window and emits a single stable event per file path.

  3. Adapter parser — Tool-specific parsers extract semantic changes from file diffs:
    • KiCad adapter: Parses .kicad_sch and .kicad_pcb S-expression format. Detects added/removed/modified components, net changes, footprint updates, and design rule modifications.
    • FreeCAD adapter: Reads .FCStd (ZIP-compressed XML). Extracts part tree changes, dimension modifications, constraint updates, and material assignments.
    • VS Code adapter: Parses firmware source files (.c, .h, .py). Detects pin mapping changes (pinmap.json), configuration constant modifications, and HAL layer updates.
  4. Event enrichment — Each parsed change becomes a GraphEvent with actor_id = "user:<id>" (from the authenticated IDE session), linking the mutation to the human engineer in the audit trail.

Adapter Interface

class FileChangeAdapter(ABC):
    """Base class for tool-specific file change parsers."""

    @abstractmethod
    async def can_handle(self, file_path: Path) -> bool:
        """Return True if this adapter handles the given file type."""
        ...

    @abstractmethod
    async def parse_changes(
        self,
        file_path: Path,
        previous_hash: str | None,
        actor_id: str,
        session_id: str,
    ) -> list[GraphEvent]:
        """Parse file changes into graph events."""
        ...

Human-Edit-Triggered Workflow

When the change detection layer produces GraphEvent records from a human edit, the following end-to-end flow executes:

sequenceDiagram
    participant ENG as Engineer
    participant IDE as KiCad / FreeCAD
    participant WF as watchfiles
    participant AP as Adapter Parser
    participant K as Kafka
    participant CON as Event Consumer
    participant NEO as Neo4j
    participant CE as Constraint Engine
    participant WS as WebSocket
    participant IA as IDE Assistant

    ENG->>IDE: Edit & save design file
    IDE->>WF: File system event
    WF->>AP: Debounced file path
    AP->>K: Publish GraphEvent[]<br/>to "artifact.ingest"
    K->>CON: Consume events
    CON->>NEO: Apply mutations<br/>(optimistic concurrency)
    NEO-->>CON: Committed (version++)
    CON->>K: Publish to "graph.mutations"
    K->>CE: Constraint evaluation triggered
    CE->>CE: evaluate_change()<br/>(validation report)
    CE-->>K: Publish constraint results
    K->>WS: Broadcast to subscribers
    WS->>IA: Notification payload
    IA->>ENG: Display validation result<br/>(pass / warnings / violations)

Key Difference: Validation Timing

In Autonomous Mode, the constraint engine runs evaluate_change() before the mutation is committed to the graph. If constraints are violated, the mutation is rejected and the agent must revise its proposal.

In Assistant Mode, the change is applied first — the system trusts the engineer’s intent. The constraint engine then runs evaluate_change() after the mutation is committed, producing a validation report that is delivered to the engineer via the IDE Assistant. This preserves the engineer’s workflow (their save is never blocked) while still ensuring constraint awareness.

Scenario Autonomous Mode Assistant Mode
Constraint violation Mutation rejected — agent retries Mutation applied — warning surfaced to engineer
Constraint pass Mutation committed Mutation committed (no notification needed)
Parse failure N/A (agents emit structured events) artifact.ingest_failed event published — engineer notified

Ingest Failure Handling

If the adapter parser cannot extract semantic changes from a file (corrupted file, unsupported format version, ambiguous diff), it publishes an artifact.ingest_failed event instead of artifact.ingested. The IDE Assistant notifies the engineer with the parse error details, and the file-world state diverges from the graph until the issue is resolved (see Drift Detection below).


Bidirectional Artifact Synchronization (Drift Detection)

The Digital Twin Evolution architecture defines a dual state machine with Semantic State (graph) and Artifact State (files). Drift occurs when these two states diverge. There are two drift directions:

flowchart LR
    subgraph FILE["File World"]
        direction TB
        F1["KiCad .kicad_sch"]
        F2["FreeCAD .FCStd"]
        F3["firmware/*.c"]
    end

    subgraph GRAPH["Design Graph (Neo4j)"]
        direction TB
        G1["Schematic nodes"]
        G2["Mechanical nodes"]
        G3["Firmware nodes"]
    end

    FILE -->|"Human edit:<br/>File newer than graph"| GRAPH
    GRAPH -->|"Agent mutation:<br/>Graph newer than file"| FILE

    REC["Periodic Reconciler<br/>actor_id = system:reconciler"] -.->|"compare StateLink<br/>hashes"| FILE
    REC -.->|"compare StateLink<br/>versions"| GRAPH

    style FILE fill:#3498db,color:#fff
    style GRAPH fill:#2C3E50,color:#fff
    style REC fill:#E67E22,color:#fff

Drift Direction 1: File Newer Than Graph (Human Edit)

The engineer saves a file that the change detection layer has not yet ingested, or the ingest pipeline failed. The file-world is ahead of the graph.

Resolution: The change detection pipeline processes the file and publishes artifact.ingested events. If ingest failed previously, re-running the adapter parser (after the engineer fixes the file or the adapter is updated) resolves the drift.

Drift Direction 2: Graph Newer Than File (Agent Mutation)

An agent proposes a semantic mutation that is committed to the graph, but the corresponding artifact file has not yet been re-projected via MCP adapters. The graph is ahead of file-world.

Resolution: The projection pipeline (MCP adapters) generates updated tool-specific files from the new graph state. The IDE Assistant notifies the engineer that files have been updated and shows a diff.

Periodic Reconciler

A background reconciler runs on a configurable interval (default: 60 seconds) to detect drift that the event pipeline missed (e.g., files modified while the system was offline, or projection failures).

class DriftReconciler:
    """Periodic check for file-world ↔ graph-world divergence."""

    async def reconcile(self, project_path: Path) -> list[DriftReport]:
        """Compare StateLink records against current file hashes."""
        reports = []
        for link in await self.get_state_links():
            file_hash = await self.hash_file(project_path / link.artifact_path)
            if file_hash != link.file_hash:
                report = DriftReport(
                    artifact_path=link.artifact_path,
                    direction="file_newer" if file_hash else "file_missing",
                    semantic_version=link.semantic_version,
                    expected_hash=link.file_hash,
                    actual_hash=file_hash,
                )
                reports.append(report)
                await self.publish_event(GraphEvent(
                    event_type=EventType.DRIFT_DETECTED,
                    actor_id="system:reconciler",
                    properties={
                        "artifact_path": link.artifact_path,
                        "direction": report.direction,
                        "semantic_version": link.semantic_version,
                    },
                ))
        return reports

The reconciler uses the StateLink model defined in Digital Twin Evolution to compare the semantic_version and git_commit recorded at projection time against the current file hashes and graph version.


New Event Types

Five new values for the EventType enum and two new Kafka topics support the Assistant Mode pipeline.

EventType Additions

class EventType(str, Enum):
    # ... existing types ...

    # Assistant Mode — human-edit ingestion
    ARTIFACT_FILE_CHANGED = "artifact.file_changed"     # watchfiles detected a file change
    ARTIFACT_INGESTED = "artifact.ingested"              # Adapter parser successfully extracted graph events
    ARTIFACT_INGEST_FAILED = "artifact.ingest_failed"    # Adapter parser failed to extract changes

    # Drift detection — file ↔ graph reconciliation
    DRIFT_DETECTED = "drift.detected"                    # Reconciler found divergence
    DRIFT_RESOLVED = "drift.resolved"                    # Divergence resolved (re-ingest or re-project)
Event Type Actor Published When
artifact.file_changed user:<id> watchfiles detects a design file modification
artifact.ingested user:<id> Adapter parser successfully converts file changes to graph events
artifact.ingest_failed user:<id> Adapter parser cannot extract semantic changes from file
drift.detected system:reconciler Periodic reconciler finds file-world ↔ graph-world divergence
drift.resolved system:reconciler Drift is resolved by re-ingestion or re-projection

Kafka Topic Additions

topics:
  # ... existing topics ...
  - "artifact.ingest"     # Human-edit ingestion events (file_changed, ingested, ingest_failed)
  - "twin.drift"          # Drift detection and resolution events
Topic Retention Rationale
artifact.ingest 30 days Human-edit events — replayable from file history
twin.drift 7 days Drift detection is ephemeral diagnostic data

These event types and topics are also specified in Event Sourcing.


Relationship to ADR-001

Assistant Mode reuses the same Pydantic AI + Temporal infrastructure established in ADR-001.

Component How Assistant Mode Uses It
Temporal Activities The ingest pipeline (adapter parsing → graph mutation) runs as a Temporal activity with retry semantics. If the adapter parser fails transiently (file locked, Neo4j unavailable), Temporal retries with exponential backoff.
Temporal Signals IDE Assistant notifications use the same Temporal signal mechanism as EVT/DVT/PVT gate approvals. The constraint engine publishes validation results as signals that the IDE Assistant workflow receives.
Pydantic AI Agents The adapter parsers are not agents (they are deterministic parsers), but the constraint evaluation triggered by human edits invokes the same Pydantic AI-backed constraint engine used in Autonomous Mode.
MCP Servers Artifact projection (graph → file) uses the same MCP server connections (KiCad MCP, FreeCAD MCP) that agents use for autonomous artifact generation.
Pydantic Models GraphEvent, StateLink, DriftReport, and adapter output models are Pydantic models — consistent with the Pydantic-first validation approach across the entire stack.

Ingest Activity Definition

@activity.defn
async def ingest_file_change(
    file_path: str,
    previous_hash: str | None,
    actor_id: str,
    session_id: str,
) -> dict:
    """Temporal activity: parse a file change and apply to the graph.

    Retries on transient failures (file locked, Neo4j unavailable).
    Publishes artifact.ingested or artifact.ingest_failed.
    """
    adapter = adapter_registry.get_adapter(Path(file_path))
    if adapter is None:
        await publish_event(EventType.ARTIFACT_INGEST_FAILED, actor_id, {
            "file_path": file_path,
            "reason": "no_adapter",
        })
        return {"status": "no_adapter"}

    events = await adapter.parse_changes(
        Path(file_path), previous_hash, actor_id, session_id,
    )
    for event in events:
        await publish_to_kafka("artifact.ingest", event)

    await publish_event(EventType.ARTIFACT_INGESTED, actor_id, {
        "file_path": file_path,
        "event_count": len(events),
    })
    return {"status": "ingested", "event_count": len(events)}

Document Description
Event Sourcing Event stream architecture — defines GraphEvent, EventType enum, Kafka topics, and concurrency model used by both modes
Digital Twin Evolution Dual state machine (Semantic State ↔ Artifact State), StateLink model, and the drift detection concept that this document specifies
Constraint Engine Engineering constraint evaluation — evaluate_change() runs after-apply in Assistant Mode, before-commit in Autonomous Mode
System Vision Architectural principles: “Twin Owns Semantic Truth” and “Files Are Projections” — both modes honor these principles
ADR-001: Agent Orchestration Pydantic AI + Temporal infrastructure reused by the ingest pipeline and IDE notifications
Architecture Overview System architecture, watchfiles dependency, and IDE Assistant component
Agent Chat Channel Real-time conversational interface — extends the assistant paradigm with bidirectional engineer-agent dialogue
Product Roadmap Three-phase product roadmap (orthogonal to the runtime mode distinction)

Document Version: v1.0 Last Updated: 2026-02-28 Status: Technical Architecture Document

← ADR-001: Agent Orchestration Architecture Home →