Assistant Mode: Dual-Mode Operation
Human edits and AI workflows share one Design Graph through a unified event pipeline
Table of Contents
- The Two Runtime Modes
- Change Detection Layer
- Human-Edit-Triggered Workflow
- Bidirectional Artifact Synchronization (Drift Detection)
- New Event Types
- Relationship to ADR-001
- 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
-
watchfileswatcher — Monitors the project directory for file changes using OS-native file system events (inotifyon Linux,FSEventson macOS,ReadDirectoryChangesWon Windows). Filters to known artifact extensions (.kicad_sch,.kicad_pcb,.FCStd,.step,.py,.c,.h). -
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.
- Adapter parser — Tool-specific parsers extract semantic changes from file diffs:
- KiCad adapter: Parses
.kicad_schand.kicad_pcbS-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.
- KiCad adapter: Parses
- Event enrichment — Each parsed change becomes a
GraphEventwithactor_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)}
Related Documents
| 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 → |