Interactive workflows

人、agent 和 live KLayout 一起工作

让人和 agent 在同一个 live KLayout 上协作的一组交互能力:SEND 选择记忆一个 agent 控制多个 KLayout跨会话搬运、8082 端口 klive 兼容显示,以及把整段会话录成可回放脚本。以下每一项都是普通工具,除插件外无需任何特殊配置。

一份目录,两张脸

klink 的控制面有两张脸:Python client KLinkClient,以及 MCP server 暴露的同名工具。二者都来自 live plugin 的 method registry,不维护一份会漂移的手写清单。全部工具的功能、参数与示例见 MCP 工具参考;本页讲的是把这些工具串成人和 agent 一起协作的交互流程——每一节都是"是什么 → 人怎么点 → agent 调什么、真实返回什么",配真实截图,当页看完,不用再跳去别处找例子。

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

SEND 选择记忆 —— "这块区域"到底指哪

人和 agent 协作最大的摩擦,是"我说的'这块'是哪块"。klink 用 SEND 解决它:你在 KLayout 里框选一块几何,点插件工具栏的 SEND 按钮,这次选择就被记成一个稳定 id(如 sel_0006),存进 session 级记忆 .klink/sessions/<id>/interaction_context.jsonl。从此"刚发的""这里""那一个"都能解析到精确几何,而不是截图

# 人的做法:框选一块几何 → 点工具栏 SEND
# agent 的等价调用:
selection.send_context(source="tutorial_demo")
# → {"status": "sent", "count": 5, "send_seq": 5}
KLayout 窗口,SEND 按钮被红框圈出并有箭头指向说明,画布里的器件被青色框圈出标为已选中的 5 个对象
选中器件(青框,5 个对象已高亮)→ 点工具栏 SEND。返回 status: sent · count 5 · send_seq 5——这块选区现在是 agent 记忆里一条持久条目(形如 sel_0005),之后"我刚发的那个"就指它。
工具用途
interaction.selection.recent最近若干次 SEND(默认最新 5,按顺序不按时间)。
interaction.selection.latest最新一次 SEND。
interaction.selection.get按 id 读精确记录。
interaction.selection.label给重要选择加名字/描述。
interaction.context同时返回 live 选择 + 最近 SEND 记忆。

关键区别:selection.get当前 live 选择interaction.*明确 SEND 过的持久记忆。因为从 SEND 到那句引用它的话,中间可能隔了好几分钟的版图操作,所以记忆按序号/数量解析,而不是按时间。用 interaction.context 可以同时拿到"当前选择"和"最近 SEND",让 agent 判断用户指的是哪个。

两个刻意的设计取舍:一是用显式 SEND,而不是被动监听每次点选——只有用户主动发出的选择才记一条 sel_000N,过滤掉探索性点击和误选的噪音;二是记忆放在插件外部(桥侧),插件只保持"此刻"的零层事实(selection.get 和事件流),会话级记忆、id 分配、语言解析都在 MCP 桥的 interaction.*,按会话持久化到磁盘 JSONL。

SEND 是持久的、不丢的。 插件在广播前就用单调递增的 send_seq 把每次 SEND 写进 journal,所以即使当时没有 agent 在监听也不会丢——返回 status: journaled_no_listener 同样算成功;桥下次连上时会从 journal 补齐并按 send_seq 去重。

第二个例子,同一个按钮:硅光端口。 选中 net link0 上的两个端口(自定义器件的 P0、MMI 的 o1)→ SEND,字段一样,只是数字跟着换:

selection.send_context(source="gf_tutorial")
# → {"status": "sent", "count": 2, "send_seq": 2}
KLayout 工具栏,SEND 按钮被红框圈出并有箭头指向说明;左侧 cell 树列出 klink_port.P0 (link0) 和 MMI 的端口
硅光场景:点 SEND 把两个 link0 端口发给 agent,返回 count 2 · send_seq 2。左侧 cell 树能看到 klink_port.P0 (link0) 和 MMI 的端口。

一个 agent 控制多个 KLayout

每个 KLayout 窗口启动时绑定一个端口(第一个空闲的 87658799)并注册为一个 session,工具栏上的 K876x 标志就是这个窗口的自我标识。一个 MCP 桥可以同时驱动全部这些 session——它们是平等 peer,没有"工作端口"或"LVS 端口"的特殊角色,agent 靠显式的 session 参数寻址,不靠"当前前台窗口"。

KLayout 窗口顶部工具栏,右侧的 K8765、SEND、GFTGT、REC 四个控件被红框圈出,各自连一条箭头指向说明标签
klink 插件工具栏(红框为标注叠加,非 KLayout 原生):K8765 是端口标志,说明这个窗口是会话 8765;SENDGFTGTREC(录制)是人和 agent 共用的三个入口,后两个分别在下面的"跨会话搬运/klive"节和"录制"节里讲。

因为窗口靠端口而不是靠"当前焦点"寻址,agent 可以在一个窗口里读、在另一个窗口里写,不用来回切前台:

两个 KLayout 窗口并排,左窗口标志 K8765 里有一个器件,右窗口标志 K8767 里只有一个空边框,两个端口标志各自被红圈圈出
同一个 agent 眼里的两个 live 窗口:左 K8765(源,含器件),右 K8767(目标,空框)。agent 调用时传 session="8765""8767" 就能分别操作,互不干扰。
klayout-8765真实工作版图
klayout-8766scratch / 测试
klayout-8767参考 / 对比
一个 MCP 桥按需切换目标
工具用途
klink.session_list枚举正在运行的 session。
klink.session_label给某个 session 打人类 label / 别名。
klink.session_resolve把 label / 别名 / active cell / top cell 解析成 session id。
klink.session_use把桥的主 RPC 目标切到某个 session。
klink.session_status看某个 session 的记录。
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"

实践建议:同时开着真实工作版图和 demo/测试窗口时,先给每个窗口打 label,agent 就能用"scratch"这种名字而不是端口号来指代,破坏性操作也不会误伤工作 tab。

跨会话搬运 —— 两阶段、确认安全

在窗口之间搬几何是两阶段的:先 prepare,对目标 session dry-run 通过后,再 commit 才真正写入。这样在写进目标窗口之前就能发现"搬错窗口""包内容不对"。

选择源几何源 KLayout 里 SEND 或指定 cell/selection。
prepare构建包并对目标 session dry-run。
检查报告确认目标、cell、layer、shape/instance 统计。
commit确认后才写入目标窗口。
# 源窗口 K8765 里已经 SEND / 选中了 5 个对象(见上一节)
# 阶段一:prepare —— 读源选区、打包、在目标窗口 K8767 的 MW_DST 上 dry-run
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}}

# 阶段二:commit —— 确认后真正写入
klink.transfer_commit package_id="pkg_0001"
# → commit: {"cell":"MW_DST","requested":5,"inserted":5,"by_layer":{"10/0":3,"20/0":2}}
K8767 窗口,只有一个空的粉色着陆框和 DST·K8767 文字,框里没有器件
transfer 之前:K8767MW_DST 里只有一个空着陆框。
同一个 K8767 窗口,空框里现在多了从 K8765 搬过来的器件,器件被青色框圈出
commit 之后:器件从 K8765 落进了 K8767requestedcommit.inserted 都是 5、by_layer 逐层一致(10/0 × 3 + 20/0 × 2),跟上一节 SEND 选中的那 5 个对象对得上;源窗口 K8765 原封不动——transfer 是拷贝,不是剪切。

copy_modeflat_selection(默认)只拍平复制可见几何、不带层级;shallow_instance 只搬实例引用——若目标窗口缺对应子 cell,它会阻断提交而不是静默造一个空壳。目标窗口写入前什么都不会真正写进去,所以搬错目标会在 dry-run 阶段被发现。搬运中还可用 layer_map 重映射图层、translate_um 平移。

想看工具栏解剖 + 两窗口 + SEND + GFTGT + transfer 连成一整套的完整实战,见分步教程:跨窗口协作

8082 端口:klive 兼容显示,完全替代 klive

插件除了 8765 的 RPC,还在 127.0.0.1:8082 起了一个 klive 协议兼容的显示服务。它是原版 klive直接替代(drop-in replacement):任何硬编码 localhost:8082 的 gdsfactory / 外部脚本——尤其是 Component.show()——无需改动就能把版图推进 KLayout。你不需要再单独装 klive。

多窗口时有个现实问题:Component.show() 该落进哪一个 KLayout 窗口?klive 兼容端口是固定的 8082,工具栏的 GFTGT 按钮就是用来把这条流量指到当前窗口——人怎么点:在想接收 gdsfactory 版图的窗口上点一下 GFTGT;agent 等价调用是 session.mark_klive_target

# 人的做法:在想接收 gf 版图的窗口上点 GFTGT
# agent 的等价调用:在 K8767 上执行
session.mark_klive_target()
# → {"ok": true, "klive_target_session": "klayout-8767"}
K8767 窗口,GFTGT 按钮被红框圈出并有箭头指向说明标签
K8767 上点 GFTGTklive_target_session 被设成 klayout-8767。此后经 8082 推来的 gdsfactory 版图都会落进这个窗口,而不是默认的第一个;想换一个窗口接收,就在那个窗口上再点一次 GFTGT。
能力说明
协议兼容逐字节兼容 klive 0.4.1 的请求/响应({"gds":…, "keep_position":…, "libraries":…, "technology":…, "lyrdb":…, "l2n":…}),并回 {"version":"0.4.1", "type":"open"|"reload", …}
c.show() 开箱即用gdsfactory 的 Component.show() 直接把组件推进 KLayout,已开则 reload、可 keep_position 保住视角。
不止 GDS随 klive 协议一并推送 lyrdb(DRC 标记)和 l2n(网表抽取),一并在浏览器里显示。
多会话转发单窗口时直接走 pya 载入;多窗口时 8082 是固定入口,把请求转发给注册的目标 session——用 klink.session_set_klive_target / session.mark_klive_target(就是 GFTGT 按钮的等价调用)选定哪个 KLayout 接收。
不干扰 RPC即使 8082 被占用起不来,也只影响显示;8765 的 klink RPC 完全不受影响。
import gdsfactory as gf
c = gf.components.mzi()
c.show()          # 走 127.0.0.1:8082 → klink 的 klive 兼容服务 → 显示在 GFTGT 标记的窗口
这也是硅光流的显示通道:photonics.import_gf 把成品 gdsfactory 脚本接管进 klink 后,后续的 photonics.reroute 也通过同一条链路刷新显示。

录制 → 可回放脚本

是什么:recorder 把一整段工作会话——手工 GUI 编辑和 agent 的 RPC 编辑一视同仁——变成可回放脚本。它不是逐条 RPC 日志,而是 replay-script 生成器:记录足以重建最终 layout 状态的动作,所以一个批量 RPC 或一段 exec.python 可能会展开成逐对象的动作。

人怎么点:工具栏的 REC 按钮开始/停止录制,和下面的 recorder.* 调用是同一件事的两个入口。

工具用途
recorder.start开始录制(可选 output_path 指定输出)。幂等。
recorder.status是否在录、已记录多少事件/动作、输出路径。随时可调。
recorder.stop停止并写脚本,返回统计 + wrote。幂等。

调用什么、拿到什么:停止时写出两个产物——

  • <name>.py —— 基于 KLinkClient 的回放脚本,并用 # user command: 注释标出每步对应的菜单动作。
  • <name>_pya.py —— 独立 pya 版,可在没装 klink 的 KLayout 里直接跑。
recorder.status                 # 先确认没在录别人的
recorder.start
# …做一些 typed RPC 编辑 + 手工 GUI 编辑…
recorder.stop                   # 产出 .py 和 _pya.py
开始你自己的录制前先看 recorder.status,避免覆盖一个正在进行的录制。

这样一来,原本一次性的手工修改就变成了能重复运行、能改参数的脚本,agent 也能在此基础上重新生成。

Profiles 与工具发现

是什么:--profile 同时按意图(read/write/verify/escape/all)和领域(11 个 token 之一)过滤 MCP 工具列表,避免一次性把几百个工具全塞给 agent。

怎么用:启动 MCP 桥时用 --profile 指定;工具列表定下来之后,任务中途还可以调 klink.find_tools 在这份列表里按 domain 或关键词导航到更窄的一组。

python -m klink.mcp --profile read,write,verify,escape   # 默认
python -m klink.mcp --profile read,device_photonics
klink.find_tools domain="routing_backends"
klink.status                                             # 解释器 / 能力 / 连接状态

完整说明见 MCP 参考 · Profiles

Escape hatch

是什么:exec.python 可在 KLayout 内跑受控 pya 片段,适合 typed RPC 暂未覆盖的边角场景。

怎么用、要注意什么:调用方式和普通 RPC 一样,但优先级应低于 typed RPC——后者有输入验证、结构化错误、next_action,且在 recorder 里表现为"意图"而非不透明代码。exec.python 仍会触发 recorder + layout-diff 检测,所以事后能看出改了什么,但事前拿不到 typed RPC 那样的输入校验。详见 MCP 参考 · Escape hatch