The standard pattern for AI-driven DCC work runs the agent in a separate process and reaches into the DCC through a bridge — WebSocket, RPC, stdio, a subprocess. The DCC is a service the agent calls. That shape has a ceiling: every interaction is a round-trip, every tool is a marshalling problem, and the agent never actually lives inside the creative environment.
SYNAPSE inverts that. The Claude Agent SDK runs inside Houdini's own Python interpreter, dispatching tools as direct in-process calls against hou. The WebSocket survives as a thin JSON-RPC adapter for external clients during migration, but the core loop is native. Same refactor pattern composes across the portfolio to Moneta (Nuke), Octavius, and the Cognitive Bridge.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart LR
subgraph OUT ["Outside-in — the standard pattern"]
direction LR
A1["Agent process"] -.WebSocket / RPC.-> H1[("Houdini<br/>hou.*")]
end
subgraph IN ["Inside-out — SYNAPSE"]
direction LR
H2[("Houdini<br/>hou.*")]
subgraph DAEMON ["Agent daemon (thread)"]
A2["Agent SDK"] --> D["Dispatcher"]
end
D -- in-process call --> H2
end
OUT ~~~ IN
The flip changes more than transport. Tools become direct calls. Errors keep their stack trace. And — the part Sprint 3 is wiring now — events flow the other way. Houdini taps the agent on the shoulder when something cooks, instead of the agent polling to ask. See Perception channel below.
Once the daemon boots inside graphical Houdini, three threads are in play: main (Qt event loop + hou.*), daemon (the agent loop), and a short-lived worker for each main-thread dispatch (so the daemon thread can enforce a timeout on blocking hdefereval calls). Tools are pure-Python functions under synapse.cognitive.tools.* behind a Dispatcher interface. The Dispatcher composes suppress_modal_dialogs() around main_thread_exec() so every tool call gets a narrowly-scoped dialog-suppression window — the artist's own UI stays untouched outside tool dispatches.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart TB
subgraph HOU ["Graphical Houdini process"]
direction TB
subgraph MT ["Main thread — Qt event loop + hou.*"]
HUI["Houdini UI"]
HAPI["hou.* API surface"]
end
subgraph DT ["Daemon thread — synapse.host.daemon"]
AL["Agent SDK loop<br/>(anthropic)"] --> DISP["Dispatcher<br/>synapse.cognitive.dispatcher"]
DISP --> EXEC["main_thread_exec<br/>+ suppress_modal_dialogs"]
end
subgraph COG ["Cognitive layer — zero hou imports"]
TOOLS["synapse.cognitive.tools.*<br/>(inspect_stage, ...)"]
end
EXEC -. hdefereval .-> HAPI
DISP -- resolves --> TOOLS
TOOLS -. pure Python .-> HAPI
end
EXT["External MCP clients<br/>(Claude Desktop, CLI)"] -.WS JSON-RPC.-> DISP
The cognitive/ vs host/ code split is structural. synapse.cognitive.* is pure Python, zero hou imports, enforced by a grep-based lint test at CI time (tests/test_cognitive_boundary.py). synapse.host.* is Houdini-specific — hou, hdefereval, Qt thread marshaling — and gets swapped per DCC. The substrate composes.
Sprint 3 is wiring the agent's first eyes. The Dispatcher gives the agent hands; the Agent SDK gives it a brain; the perception channel lets it see what Houdini sees, in the same heartbeat as the scheduler. Two bridges compose to deliver that:
TopsEventBridge(Spike 3.1, Phase A) — registerspdg.PyEventHandlerinstances against each TOP network's livepdg.GraphContext, surfaces 7 cook + work-item events as typedTopsEventpayloads. The handler readspdg.*properties only — nohou.*calls inside — because PDG events may fire on a non-main thread (cook thread, scheduler worker). That defensive shape means the bridge is thread-safe regardless of which thread fired the event.SceneLoadBridge(Spike 3.2, Phase B) — subscribes tohou.hipFile.AfterLoadand orchestrates an injectedTopsEventBridge'scool_all/warm_allcycle on every scene load. Mile 4's empirical audit captured all four hipFile events firing onMainThread(is_main_thread=True), so the AfterLoad handler callshou.*andtops_bridge.*directly — nohdeferevalmarshaling. Adding it would be cargo-cult dispatch from main thread back to itself.
Composition, not inheritance: SceneLoadBridge(tops_bridge=...). Each class keeps a single responsibility, and the relationship is testable end-to-end with mocks.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart TB
subgraph SCENE ["Houdini scene lifecycle"]
HIP["hou.hipFile<br/>events"]
end
subgraph PDG ["PDG cook lifecycle"]
GC["pdg.GraphContext<br/>events"]
end
subgraph BRIDGE ["synapse.host.* (in-process)"]
SLB["SceneLoadBridge<br/>scene_load_bridge.py"]
TEB["TopsEventBridge<br/>tops_bridge.py"]
SLB -- "composition<br/>(tops_bridge=...)" --> TEB
end
subgraph COG ["synapse.cognitive.*"]
CB["perception_callback"]
end
HIP -- AfterLoad --> SLB
GC -- "CookComplete<br/>WorkItemResult<br/>+5 more" --> TEB
TEB --> CB
The end-to-end event flow when an artist opens a scene and cooks a TOP network:
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569','actorBkg':'#1e293b','actorTextColor':'#f1f5f9','actorBorder':'#0f172a','signalColor':'#f59e0b','signalTextColor':'#f1f5f9','noteBkgColor':'#334155','noteTextColor':'#f1f5f9','sequenceNumberColor':'#f1f5f9','labelBoxBkgColor':'#1e293b','labelTextColor':'#f1f5f9','loopTextColor':'#f1f5f9'}}}%%
sequenceDiagram
autonumber
actor Artist
participant Main as Main thread<br/>(Qt + hou.*)
participant SLB as SceneLoadBridge
participant TEB as TopsEventBridge
participant Cook as Cook thread
participant Agent as Agent perception
Artist->>Main: File → Open scene.hip
Main->>SLB: hou.hipFile event:<br/>BeforeLoad → BeforeClear →<br/>AfterClear → AfterLoad
Note over SLB: Filter holds — only<br/>AfterLoad triggers warm
SLB->>TEB: cool_all() (stale subs)
SLB->>TEB: warm_all() (walk topnets)
Note over SLB,TEB: per-topnet: register<br/>pdg.PyEventHandler against<br/>live GraphContext
Artist->>Main: cook TOP network
Main->>Cook: dispatch graph cook
Cook->>TEB: pdg.Event(WorkItemResult)
TEB->>Agent: TopsEvent (typed payload)
State today: scaffolded and tested in standalone mode (no live Houdini). The two bridges have 71 tests passing between them — 47 across tests/test_tops_bridge.py (Spike 3.1 basic + hostile) and 24 across tests/test_scene_load_bridge.py (Spike 3.2 basic + hostile). Live cook integration — the first real pdg.Event reaching the agent's perception layer in graphical Houdini — lands at Mile 5 (Spike 3.3).
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart TB
subgraph SUB ["Cognitive substrate — USD + LIVRPS"]
USD[("Session stages<br/>Agent Asset stages<br/>Turn history (append-only)<br/>Rollouts (sibling prims)")]
end
subgraph HOSTS ["DCC hosts (inside-out)"]
direction LR
SYN["SYNAPSE<br/>(Houdini) — shipping"]
MON["Moneta<br/>(Nuke) — planned"]
OCT["Octavius<br/>— planned"]
end
CB["Cognitive Bridge<br/>peer-discovery + handoff"]
SYN --- SUB
MON --- SUB
OCT --- SUB
CB --- SUB
SYN <-. peer .-> CB
MON <-. peer .-> CB
OCT <-. peer .-> CB
Each host ships its own synapse.host.* layer. The cognitive substrate — USD stage layout, LIVRPS composition semantics, the Dispatcher contract, the append-only turn history — is shared. When all three are up, they coordinate through the Bridge via filesystem peer discovery.
Tested on Windows 11 + Houdini 21.0.671. Linux / macOS paths are the same shape, different separators.
git clone https://github.com/JosephOIbrahim/Synapse.git C:\Users\%USERNAME%\SYNAPSE
cd C:\Users\%USERNAME%\SYNAPSEYou're good if: git log -1 --oneline shows the latest commit on master.
If you see fatal: destination path already exists: pick a different destination or remove the existing folder first.
Create %USERPROFILE%\houdini21.0\packages\Synapse.json:
{
"env": [
{ "PYTHONPATH": "C:/Users/YOUR_USERNAME/SYNAPSE/python" }
]
}Replace YOUR_USERNAME with your actual Windows username (forward slashes in the path, not backslashes).
You're good if: launching Houdini and running import synapse; print(synapse.__version__) in the Python Shell prints a version string.
If you see ModuleNotFoundError: No module named 'synapse': double-check the PYTHONPATH value in the .json points at the python/ directory, not the repo root.
Current primary — env var. Set ANTHROPIC_API_KEY in your system environment (not just a terminal session — Houdini launches don't inherit shell-scoped vars on Windows):
setx ANTHROPIC_API_KEY "sk-ant-..."Launch a fresh Houdini after running setx — the new value only reaches processes started after.
Forward-compat — hou.secure. When SideFX ships a secure-credentials API in a future Houdini release, SYNAPSE's auth resolver picks it up automatically. Confirmed not present in Houdini 21.0.671 (dir(hou) only exposes secureSelectionOption). No action needed today.
You're good if: in Houdini's Python Shell, import os; print(bool(os.environ.get('ANTHROPIC_API_KEY'))) prints True.
If you see False: the variable didn't land in this Houdini's environment. Close Houdini, re-open from a fresh shell, try again.
In Houdini's Python Shell:
from synapse.host.daemon import SynapseDaemon
daemon = SynapseDaemon()
daemon.start()
print("running:", daemon.is_running)
daemon.stop()You're good if: prints running: True and stops cleanly.
If you see DaemonBootError: hou.isUIAvailable() returned False: you're in headless hython, not graphical Houdini. The daemon refuses to boot in PDG / render-farm contexts (Fork Bomb prevention). For tests, pass boot_gate=False.
If you see DaemonBootError: No Anthropic API key available: step 3 didn't take. Re-launch Houdini from a fresh shell.
If you see DaemonBootError: anthropic SDK is not installed: this shouldn't happen — the SDK is vendored at python/synapse/_vendor/ and prepended to sys.path on import synapse. If it does, confirm the vendored tree is intact on disk (ls python/synapse/_vendor/anthropic/).
| Layer | State |
|---|---|
Cognitive substrate (Dispatcher + AgentToolError + cognitive/host split) |
Shipping. Zero-hou boundary enforced by lint. |
| Agent SDK loop (Anthropic, cancel-event-aware, serializable tool errors) | Shipping. Mocked end-to-end tests green. |
| Daemon lifecycle (boot gate, auth resolver, dialog suppression, bootstrap locks) | Shipping. Windows WindowsSelectorEventLoopPolicy + PYTHONNOUSERSITE + no-runtime-pip all baked. |
TurnHandle async result envelope (Spike 2.4) |
Shipping. submit_turn returns a handle immediately; submit_turn_blocking for headless / non-main-thread callers. Deadlock-pinned by 31 unit tests + regression class. |
| Vendored Anthropic SDK | Shipping. 15 MB at python/synapse/_vendor/, Python 3.11 / win_amd64 ABI lock. |
Perception channel — TopsEventBridge (Spike 3.1) |
Scaffolded. 47 tests across basic + hostile. Standalone mode only. Live PDG cook lands at Mile 5. |
Perception channel — SceneLoadBridge (Spike 3.2) |
Scaffolded. 24 tests across basic + hostile. Composes a TopsEventBridge; auto-warm on hou.hipFile.AfterLoad. Live integration at Mile 5. |
| Tools ported through the Dispatcher | 1 — synapse_inspect_stage (flat /stage AST). |
| Tools still on the Sprint 2 WebSocket path | 104 — registry tools working in production, awaiting port. (Plus 6 group-info knowledge tools that don't need porting — they serve local content without Houdini.) |
The port pattern is mechanical and documented in docs/crucible_protocol.md + the spike(1) commit message. Every legacy tool gets:
- A pure-Python function under
synapse.cognitive.tools.<name>(zerohouimports). - A schema dict (description + JSON Schema) registered alongside the function.
- The WS adapter branch in
mcp_server.pyswapped fromsynapse_inspect_stage-style direct dispatch todispatcher.execute('<name>', kwargs).
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart LR
M1["Mile 1<br/>Spike 2.4<br/>deadlock close"]:::closed
M2["Mile 2<br/>Spike 3.0<br/>PDG audit"]:::closed
M3["Mile 3<br/>Spike 3.1<br/>TopsEventBridge"]:::closed
M4["Mile 4<br/>Spike 3.2<br/>SceneLoadBridge"]:::closed
M5["Mile 5<br/>Spike 3.3<br/>first event live"]:::ahead
M6["Mile 6<br/>Spike 3.4<br/>hostile crucible"]:::ahead
M1 --> M2 --> M3 --> M4 --> M5 --> M6
classDef closed fill:#1e293b,stroke:#22c55e,color:#f1f5f9
classDef ahead fill:#334155,stroke:#94a3b8,color:#cbd5e1
Mile 1 — Spike 2.4 deadlock closure. The live Crucible baseline at end of Sprint 3 Day 1 surfaced a deadlock at the daemon ↔ main-thread boundary: synchronous submit_turn parked Houdini's main thread on a result queue while the daemon thread's hdefereval dispatch waited for that same main thread to pump Qt events. Spike 2.4 closes it by changing submit_turn to return immediately with a TurnHandle — a threading.Event-backed Future analog. The caller decides when (and on which thread) to wait. Main thread stays free to pump Qt events; daemon thread keeps the agent loop; hdefereval lambdas execute because main is responsive.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569','actorBkg':'#1e293b','actorTextColor':'#f1f5f9','actorBorder':'#0f172a','signalColor':'#f59e0b','signalTextColor':'#f1f5f9','noteBkgColor':'#334155','noteTextColor':'#f1f5f9','sequenceNumberColor':'#f1f5f9','labelBoxBkgColor':'#1e293b','labelTextColor':'#f1f5f9','loopTextColor':'#f1f5f9'}}}%%
sequenceDiagram
autonumber
actor Caller
participant Daemon as Daemon thread
participant Main as Main thread<br/>(Qt + hou.*)
participant H as TurnHandle
Caller->>Daemon: submit_turn(prompt)
Daemon-->>Caller: TurnHandle (immediate)
Note over Caller,Main: Main thread free —<br/>Qt pump runs throughout
Daemon->>Daemon: run_turn (agent loop)
loop tool calls
Daemon->>Main: hdefereval lambda
Main-->>Daemon: tool result
end
Daemon->>H: _set_result(AgentTurnResult)
Caller->>H: done() / wait() / result()
H-->>Caller: AgentTurnResult
Mile 2 — Spike 3.0 PDG API audit. The pdg module surface in Houdini 21.0.671 has known divergences from prior versions and from external-LLM training data. Mile 2 ran dir() introspection against live Houdini, captured the empirical surface in docs/sprint3/spike_3_0_pdg_api_audit.md, and refuted six wrong references in the early sketch — every hou.pdg.* path missing, hou.hipFile.addEventCallback returning None (not a removable handle), pdg.PyEventCallback being the wrong name. Each of those would have crashed first contact with Houdini if Spike 3.1 had coded against the sketch verbatim.
Mile 3 — Spike 3.1 TopsEventBridge (Phase A). In-process PDG event bridge. warm(top_node) registers a pdg.PyEventHandler against the TOP network's live pdg.GraphContext (acquired via top_node.getPDGGraphContext(), never class-instantiated — that's for fresh graphs). Surfaces 7 audit-verified event types: CookStart, CookComplete, CookError, CookWarning, WorkItemAdd, WorkItemStateChange, WorkItemResult. Threading defensive: handler reads pdg.* properties only, no hou.* calls inside. 47 tests across basic happy paths and an 8-case hostile suite (handler leak, double-bridge independence, callback-raising-mid-event, topnet-deleted-mid-subscription, multi-event-type-no-loss).
Mile 4 — Spike 3.2 SceneLoadBridge (Phase B). Auto-warm wire from hou.hipFile.AfterLoad to TopsEventBridge. Composes (not inherits) — constructor takes a TopsEventBridge instance and orchestrates its cool_all / warm_all cycle on each scene load. Mile 4's empirical scene-load audit (docs/sprint3/spike_3_2_scene_load_audit.md) captured all four hipFile events firing on MainThread, so the AfterLoad handler is a direct synchronous call — no hdefereval. 24 tests across basic happy paths and a 10-case hostile suite. One fix-forward cycle during CRUCIBLE: case 6 (unsubscribe-during-handler) surfaced a real defect — warm_all kept iterating after unsubscribe returned, leaving stale subs. Reconcile step added at end of _on_after_load: if _subscribed flipped to False mid-handler, run cool_all again. The hostile test pinned the contract; the fix held it.
Workflow — the three-role pattern. Phase A and Phase B both ran the same MOE shape internally:
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1e293b','primaryTextColor':'#f1f5f9','primaryBorderColor':'#0f172a','lineColor':'#f59e0b','secondaryColor':'#334155','tertiaryColor':'#475569'}}}%%
flowchart LR
A["ARCHITECT<br/>design only<br/>(spec contract)"] -->|design.md| F["FORGE<br/>implementation +<br/>basic tests"]
F -->|system under test| C["CRUCIBLE<br/>hostile suite<br/>(adversarial posture)"]
C -->|fix-forward| F
C -->|all green| G["Phase Gate"]
classDef arch fill:#1e293b,stroke:#f59e0b,color:#f1f5f9
classDef forge fill:#1e293b,stroke:#3b82f6,color:#f1f5f9
classDef cruc fill:#1e293b,stroke:#ef4444,color:#f1f5f9
classDef gate fill:#334155,stroke:#22c55e,color:#f1f5f9
class A arch
class F forge
class C cruc
class G gate
ARCHITECT writes the design doc and never the code. FORGE implements against the spec and writes basic happy-path tests. CRUCIBLE writes hostile tests and never the implementation; when a hostile test surfaces a real defect, FORGE fixes the implementation rather than CRUCIBLE weakening the test (Commandment 7). Each role's authority is constitutionally restricted; phase boundaries gate the merge.
87c4db9 Spike 3.2 SceneLoadBridge hostile suite (CRUCIBLE) + fix-forward
4cba649 Spike 3.2 SceneLoadBridge scaffold (FORGE)
ef7d5ae Spike 3.2 SceneLoadBridge design (ARCHITECT)
9e4cc42 Spike 3.2 scene-load audit findings landed (Mile 4 audit)
a476386 Spike 3.2 scene-load API audit infrastructure
2f46590 CI repair bump checkout/setup-python (Node.js 20 deprecation)
fcd1077 CI repair gate test_live_capture body behind __main__
bb2713b Spike 3.1 TopsEventBridge hostile suite (CRUCIBLE)
89da296 Spike 3.1 TopsEventBridge scaffold (FORGE)
2aa03d9 Spike 3.1 TopsEventBridge design (ARCHITECT)
07946dc Spike 3.0 PDG API audit findings (Mile 2 audit)
6bf2f07 Spike 3.0 PDG API audit infrastructure
b1d3163 Spike 2.4 close daemon↔main-thread deadlock via TurnHandle
6e08dae Spike 2.4 add TurnHandle (Future-shaped result envelope)
Sprint 2 Week 1 (5e6fc0c) shipped the first tool (synapse_inspect_stage) end-to-end through the still-outside-in WebSocket path. Sprint 3 built the inside-out substrate alongside it — one spike at a time, with an audit-first discipline (live dir() introspection in Houdini 21.0.671 before any code lands) and a human-in-the-loop Crucible protocol (docs/crucible_protocol.md) for the parts bash cannot drive. Tagged at v5.5.0 (4faaa3a).
Spike 3.3 First TOPS event surface live [Mile 5 — needs GUI]
workitem.complete → agent perception
real .hip + real TOP cook through the bridge
Spike 3.4 Hostile TOPS Crucible [Mile 6]
event flood, malformed events, cancellation
Mile 5 is the first time a real pdg.Event reaches the agent's perception layer through the two-bridge wiring in graphical Houdini. End-to-end timing target: under 50ms from cookComplete to perception_callback invocation (in-process should be sub-ms; budget is for safety margin). Mile 6 turns the heat up — event flood (10K events / 1s), malformed events (missing fields surface as typed parse errors), cancellation mid-cook with no orphaned callbacks.
Mile 5 cannot run from bash. It needs Joe at the GUI driving a real cook against the scaffolded bridges.
python/synapse/
├── cognitive/ # zero hou imports (lint-enforced)
│ ├── dispatcher.py # Dispatcher + AgentToolError
│ ├── agent_loop.py # Anthropic SDK turn runner
│ └── tools/ # pure-Python tool implementations
├── host/ # Houdini-specific (hou / hdefereval OK)
│ ├── daemon.py # SynapseDaemon lifecycle
│ ├── main_thread_executor.py # tri-state GUI/headless/stock
│ ├── transport.py # in-process execute_python
│ ├── dialog_suppression.py # per-tool-call hou.ui guard
│ ├── auth.py # API key resolver (env var + hou.secure probe)
│ ├── turn_handle.py # Spike 2.4 — Future-shaped submit_turn return
│ ├── tops_bridge.py # Spike 3.1 — PDG event bridge (Phase A)
│ └── scene_load_bridge.py # Spike 3.2 — auto-warm on AfterLoad (Phase B)
├── _vendor/ # anthropic + deps, CP311 win_amd64
└── ... # Sprint 2 Week 1 + prior subsystems
tests/ # 2874 passing
docs/sprint3/ # audits + design contracts + continuation
docs/crucible_protocol.md # manual Crucible runbook
mcp_server.py # Sprint 2 WebSocket adapter (still shipping)
MIT. See LICENSE.
