Interactive workflows

Human, agent, and live KLayout in one loop

A set of interaction capabilities that let a human and an agent collaborate on one live KLayout: SEND selection memory, one agent driving many KLayout windows, cross-session transfer, an 8082 klive-compatible display, and recording a whole session into a replayable script. Each is an ordinary tool — nothing here needs setup beyond the plugin.

One catalogue, two faces

klink's control surface has two faces: the Python client KLinkClient and the same tools exposed over MCP. Both come from the live plugin's method registry — no hand-maintained list that can drift. Every tool's function, parameters and examples are in the MCP Tool Reference; this page ties them into an interactive loop — each section below is "what it is → what a human clicks → what an agent calls, and its real return", with real screenshots, read start to finish on this one page.

from klink import KLinkClient
with KLinkClient() as c:
    print([m["name"] for m in c.methods()["methods"]])

SEND selection memory — what "this area" means

The biggest friction in human + agent collaboration is "which area do you mean?". klink solves it with SEND: you select geometry in KLayout and click the plugin's SEND toolbar button, and the selection is recorded as a stable id such as sel_0006 in session-scoped memory (.klink/sessions/<id>/interaction_context.jsonl). From then on "just sent", "this area", "that one" resolve to exact geometry, not a screenshot.

# the human way: select geometry → click SEND on the toolbar
# the agent-equivalent call:
selection.send_context(source="tutorial_demo")
# → {"status": "sent", "count": 5, "send_seq": 5}
A KLayout window; the SEND button is boxed in red with an arrow to a label, and the device on the canvas is boxed in teal marked as 5 selected objects
Select the device (teal box, 5 objects highlighted) → click SEND on the toolbar. Returns status: sent · count 5 · send_seq 5 — this selection is now a durable entry in the agent's memory (something like sel_0005), and "the one I just sent" refers to it later.
ToolUse
interaction.selection.recentRecent SENDs (default latest 5, by order, not age).
interaction.selection.latestThe latest SEND.
interaction.selection.getRead an exact record by id.
interaction.selection.labelName/describe an important selection.
interaction.contextLive selection + recent SEND memory together.

selection.get is the live current selection; interaction.* is the durable memory of what was explicitly SENT. Because minutes of layout work can pass between a SEND and the message referring to it, memory is resolved by order/count, not age. interaction.context returns the current selection and recent SENDs together so the agent can tell which one the user means.

Two deliberate design choices: (1) explicit SEND rather than passively watching every click — only a selection the user actively sends becomes a sel_000N, filtering out exploratory-click and mis-select noise; (2) memory lives outside the plugin (on the bridge) — the plugin holds only "right-now" zero-layer facts (selection.get + the event stream), while session-scoped memory, id assignment and language resolution live in the MCP bridge's interaction.*, persisted per session to disk JSONL.

SENDs are durable and never lost. The plugin journals every SEND with a monotonic send_seq before broadcasting it, so a SEND made while no agent is listening is not lost — it returns status: journaled_no_listener, which still counts as success; the bridge catches up from the journal and deduplicates by send_seq next time it connects.

A second example, same button: photonic ports. Select two ports on net link0 (P0 on a custom device, o1 on an MMI) → SEND — same fields, just different numbers:

selection.send_context(source="gf_tutorial")
# → {"status": "sent", "count": 2, "send_seq": 2}
KLayout toolbar; the SEND button is boxed in red with an arrow to a label; the cell tree on the left lists klink_port.P0 (link0) and the MMI's ports
Photonics scenario: click SEND to hand two link0 ports to the agent, returning count 2 · send_seq 2. The cell tree on the left shows klink_port.P0 (link0) and the MMI's ports.

One agent, many KLayouts

Each KLayout window binds a port (the first free one in 87658799) and registers as a session; the toolbar's K876x badge is that window's self-identification. One MCP bridge can drive all of them at once — they are equal peers, no port has a "working" or "LVS" role; the agent addresses windows by an explicit session argument, never by "whichever window has focus".

Top of a KLayout window; the K8765, SEND, GFTGT, and REC controls at the right of the toolbar are boxed in red, each with an arrow to an explanatory label
The klink plugin toolbar (red boxes are annotation overlay, not native KLayout): K8765 is the port badge telling you this window is session 8765; SEND, GFTGT, and REC (recorder) are the three entry points humans and agents share — the latter two are covered below under "cross-session transfer / klive" and "recorder".

Because windows are addressed by port rather than by "current focus", an agent can read in one window and write in another without shuffling anything to the foreground:

Two KLayout windows side by side; the left window badged K8765 holds a device, the right window badged K8767 holds only an empty frame, each port badge circled in red
Two live windows as one agent sees them: left K8765 (source, with a device), right K8767 (target, empty frame). An agent passes session="8765" or "8767" to act on each independently, without interference.
klayout-8765real working layout
klayout-8766scratch / test
klayout-8767reference / diff
one MCP bridgeswitches target on demand
ToolUse
klink.session_listEnumerate running sessions.
klink.session_labelGive a session a human label / aliases.
klink.session_resolveResolve a label / alias / active cell / top cell to a session id.
klink.session_useSwitch the bridge's main RPC target to a session.
klink.session_statusInspect one session's record.
klink.session_list
klink.session_label session_id="klayout-8766" label="scratch" aliases=["test"]
klink.session_resolve query="scratch"     # → klayout-8766
klink.session_use session_id="klayout-8766"

In practice: when a real working layout and a demo/test window are open at once, label each window first so the agent refers to "scratch" instead of a port number and destructive ops never hit the working tab.

Cross-session transfer — two-phase, confirmation-safe

Moving geometry between windows is two-phase: prepare first, dry-run on the target, then commit to actually write. That catches "wrong target" and "wrong package" before anything lands in the target window.

Select sourceSEND or name a cell/selection.
prepareBuilds a package, dry-runs on target.
Inspect reportTarget, cell, layers, shape/instance counts.
commitOnly then write into the target.
# the 5 objects were already SENT / selected in source window K8765 (see above)
# phase 1: prepare -- read the source selection, package, dry-run on K8767's MW_DST
klink.transfer_prepare source_session="8765" target_session="8767" \
    target_cell="MW_DST" copy_mode="flat_selection"
# → prepare_dry_run: {"cell":"MW_DST","requested":5,"inserted":0,"by_layer":{"10/0":3,"20/0":2}}

# phase 2: commit -- write for real after review
klink.transfer_commit package_id="pkg_0001"
# → commit: {"cell":"MW_DST","requested":5,"inserted":5,"by_layer":{"10/0":3,"20/0":2}}
The K8767 window with only an empty pink landing frame and the text DST·K8767, no device inside
Before transfer: K8767's MW_DST holds only an empty landing frame.
The same K8767 window; the device moved over from K8765 now sits inside the frame, boxed in teal
After commit: the device has landed from K8765 into K8767. requested and commit.inserted are both 5, by_layer matches layer-for-layer (10/0 × 3 + 20/0 × 2), matching the 5 objects SENT above; the source window K8765 is untouched — transfer is a copy, not a cut.

copy_mode: flat_selection (default) flat-copies visible geometry without hierarchy; shallow_instance moves instance references — and if the target window lacks the child cell, it blocks the commit instead of silently making an empty shell. Nothing lands until commit, so a wrong target is caught at dry-run. Plus layer_map remapping and translate_um offset during transfer.

To see the toolbar anatomy, two windows, SEND, GFTGT, and transfer chained into one full walkthrough, see the step-by-step cross-window tutorial.

Port 8082: klive-compatible display, a full klive replacement

Besides RPC on 8765, the plugin runs a klive-protocol-compatible display server on 127.0.0.1:8082. It is a drop-in replacement for the original klive: any gdsfactory / external script that hard-codes localhost:8082 — especially Component.show() — pushes layout into KLayout with no changes. You do not install klive separately.

With several windows open there's a practical question: which KLayout window should Component.show() land in? The klive-compatible port is a fixed 8082, and the toolbar's GFTGT button aims that traffic at the current window — what a human clicks: GFTGT on the window that should receive gdsfactory layout; the agent-equivalent call is session.mark_klive_target:

# the human way: click GFTGT on the window that should receive the gf layout
# the agent-equivalent call, run on K8767:
session.mark_klive_target()
# → {"ok": true, "klive_target_session": "klayout-8767"}
The K8767 window; the GFTGT button is boxed in red with an arrow to an explanatory label
Click GFTGT on K8767: klive_target_session becomes klayout-8767. From then on, gdsfactory layout pushed over 8082 lands in this window instead of the default first one; to receive on a different window, click GFTGT there instead.
CapabilityDetail
Protocol compatibleByte-compatible with klive 0.4.1 request/response ({"gds":…, "keep_position":…, "libraries":…, "technology":…, "lyrdb":…, "l2n":…}) and replies {"version":"0.4.1", "type":"open"|"reload", …}.
c.show() out of the boxgdsfactory's Component.show() pushes straight into KLayout, reloads if open, keep_position keeps your view.
More than GDSThe klive protocol also carries lyrdb (DRC markers) and l2n (netlist extraction), shown in the browser.
Multi-session forwardSingle window loads via pya directly; with many windows, 8082 is a fixed entrypoint forwarding to the registered target session — pick it with klink.session_set_klive_target / session.mark_klive_target (the GFTGT button's equivalent).
Never disturbs RPCIf 8082 can't bind, only display is affected; klink RPC on 8765 is unaffected.
import gdsfactory as gf
c = gf.components.mzi()
c.show()          # via 127.0.0.1:8082 → klink's klive-compat server → shown in the GFTGT-marked window
This is also the display channel for the photonics flow: after photonics.import_gf takes over a finished gdsfactory script, later photonics.reroute refreshes the display through the same path.

Record → replayable script

What it is: the recorder turns a whole working session — manual GUI edits and agent RPC edits alike — into a replayable script. It is a replay-script generator, not a literal call log: it records whatever rebuilds the final layout state, so a bulk RPC or an exec.python snippet may expand into per-object actions.

What a human clicks: the toolbar's REC button starts/stops recording — the same thing as the recorder.* calls below, just a different entry point.

ToolUse
recorder.startBegin recording (optional output_path). Idempotent.
recorder.statusRecording? how many events/actions so far, output path. Safe anytime.
recorder.stopStop and write; returns stats + wrote. Idempotent.

What you get back: stopping writes two artifacts —

  • <name>.py — a KLinkClient replay script, annotated with # user command: lines naming each menu action.
  • <name>_pya.py — a standalone pya companion that runs inside KLayout with no klink installed.
recorder.status
recorder.start
# … typed RPC edits + manual GUI edits …
recorder.stop        # writes .py and _pya.py
Check recorder.status before starting your own recording so you never clobber one already in progress.

So a one-off manual fix becomes a script you can re-run and re-parameterize, and an agent can regenerate from it.

Profiles & tool discovery

What it is: --profile filters the MCP tool list by intent (read/write/verify/escape/all) and by domain (one of 11 tokens), so an agent isn't handed hundreds of tools at once.

How to use it: pick a profile when starting the MCP bridge; mid-task, call klink.find_tools to navigate that list further by domain or keyword.

python -m klink.mcp --profile read,write,verify,escape   # default
python -m klink.mcp --profile read,device_photonics
klink.find_tools domain="routing_backends"
klink.status                                             # interpreter / capabilities / connection status

Full detail on MCP Reference · Profiles.

Escape hatch

What it is: exec.python runs controlled pya inside KLayout for cases no typed RPC covers.

How to use it, and the catch: it's called like any other RPC, but rank it below typed RPCs — those validate input, return structured errors with a next_action, and appear in the recorder as intent rather than opaque code. exec.python still schedules recorder + layout-diff detection, so you can see what changed after the fact, but you don't get the pre-flight validation a typed RPC gives you. See MCP Reference · Escape hatch.