Julep CLI Pure-Registry 修复实录:将 `julep lint` 与 `julep run` 的校验/解释迁移到子进程 AI AgentAgent 框架后端【免费下载链接】julepJulep — durable, composable AI agents. Flows that crash and resume, retry safely, and explain every step.项目地址https://gitcode.com/gh_mirrors/ju/julep点击查看免费下载Julep 是一款 durable、composable 的 AI Agent 框架其 CLI 通过子进程导入用户模块并把 Flow 序列化为 IR再由父进程完成校验与解释。但当一个 Flow 使用pure装饰器时父进程中从未执行过该装饰器pure 注册表为空导致julep lint/julep run对任何含pure的 Flow包括官方 quickstart都报UNKNOWN_PURE。本文基于仓库中的实现计划 docs/plans/2026-06-29-ws1-ca-pure-registry-fix.md完整还原这条从根因定位到落地的修复链路为什么会出现注册表为空、为什么选择把校验与解释整体搬进子进程、echo 开发环境的提取与复用、父子进程 JSON 协议设计以及回归测试如何永久锁定该修复。读完你既能理解 Julep CLI 的 resolve 边界设计也能直接照搬这套进程边界内注册、边界内求值的架构模式。说明本文所引计划写作时包名为composable_agents仓库现已更名为julep所有源码路径与示例均已按当前仓库状态更新修复本身已在当前主分支完整落地。一、问题根因父进程里的空 pure 注册表1.1pure的注册机制在 Julep 中pure是与tool平级的声明式装饰器它把一段确定性 Python 函数注册进全局 pure 注册表供 Flow 中的arr、alt选择器/谓词、eachreducer、收敛谓词等节点按名字引用。注册只发生在装饰器执行的那一刻from julep import flow, pure, think, tool tool(effectread, idempotentTrue) def lookup(ticket: str) - dict: return {queue: billing} pure(passthrough) def passthrough(hit: dict) - dict: return {seen: sorted(hit.keys())} flow def triage(ticket: str) - dict: hit lookup(ticket) prompt passthrough(hit) answer think(reply, prompt) return prompt | answer1.2 崩溃链路IR 在子进程校验在父进程CLI 的 resolve 动作由 julep/cli/_resolve_child.py 承担它运行在用户自己的 Python 解释器进程中importlib导入用户的模块此时pure装饰器确实执行、pure 注册表是满的找到目标 Agent 后只把{ir: ..., name: ...}通过带哨兵标记的 JSON 写回 stdout。父进程拿到 IR 后validatejulep/validate.py和interpretjulep/execution/interpreter.py都在父进程执行——而父进程里pure装饰器从未跑过pure 注册表为空。校验器按名字解析 pure 时全部落空。以 julep/validate.py 为例arr节点会报UNKNOWN_PURE: arr function not registered: passthrough同类检查散布在alt选择器validate.py、二元alt谓词validate.py、收敛谓词iter_up_tovalidate.py、eachreducervalidate.py与 round notevalidate.py等处诊断码统一为UNKNOWN_PURE其展示模板定义在 julep/diagnostics.py。运行时同理interpret按名查找 pure 得到空表于是julep run triage直接报unknown pure passthrough。设计文档docs/plans/2026-06-29-composable-agents-dx-improvements.md将这一现象列为 Critical几乎每个真实 Flow 都至少用到一个pure旗舰 CLI 却无法检查或运行它们而直接python script.py却能正常工作。二、方案选型为什么选择把校验与解释搬进子进程设计 spec 曾评估过两条修复路线Approach A被否决——跨界搬运 pure 源码、父进程重注册子进程把每个被引用的 pure 源码打包进 payload父进程收到后重新注册。这条路线会破坏框架赖以成立的确定性契约pure 的 pin 就是源码哈希且父进程的全局注册表在多 Agent 场景下会跨 Flow 累积污染。Approach B采纳——校验与解释整体移入子进程让validate和interpret运行在已经导入用户模块、pure 注册表是满的那个进程里父进程退化为一个渲染子进程结果的薄调用方。julep run≡dry_run的等价性由构造保证。Approach B 的核心理念是一切依赖 pure 注册表的操作都必须运行在注册表存在的那一个进程内。它同时完美呼应了既有的freeze动作——freeze早已在子进程内调用deploy()见 julep/cli/_resolve_child.py 的_freeze_agentlint/run只是把同一模式推广到校验与解释。计划还规定了三条全局约束Pure core 不得导入temporalio只有julep/execution/允许julep/cli/可以导入 interpreter/projection现状已如此。三道质量闸门每个提交前必须全绿python -m pytest tests/cli -q必须用python -m pytest禁止裸pytest、uv run mypy --strict julep、ruff check julep。确定性契约不变pure 依然靠源码形式的原始pure注册本次只改变 validate/interpret 的运行位置不改变注册方式同时保留 IR-only 的resolve子进程动作与resolve_agent父进程函数——ls/show/graph/deploy/status都依赖它们。三、Task 1把 echo 开发环境构建器提取为_echo.py3.1 什么是echo 环境为了让julep run在无 API Key、无网络的离线条件下也能走通控制流CLI 用一组记录返回型桩echo stub替换所有外部 handler每个 tool、reasoner、subflow、agent 都返回{output: 输入}配一个自动放行的 gate 与 DEV 模式。这样 Flow 的每个分支都能被走到、每次投影事件都会被采集跑出来的是一棵完整的 trace tree——虽然值是假的控制流是真的。计划的第一步是纯重构把runner.py里内联的环境构建逻辑原样搬到新文件 julep/cli/_echo.py暴露统一的build_echo_env(node: Node) - tuple[InMemoryEnv, InMemoryProjection]。落地后的实现要点# julep/cli/_echo.py摘录 def _echo(value: Any) - dict[str, Any]: Dev stub: wrap any handlers input in a record so env folds stay happy. return {output: value} def build_echo_env(node: Node) - tuple[InMemoryEnv, InMemoryProjection]: _clear_frozen_hashes(node) # 清掉 CallStep 的 frozen_hash保证每次可重跑 projection InMemoryProjection() # 全新投影采集本次运行全部事件 env InMemoryEnv( {}, ProjectionEmitter(projection), tools_echo_tools(node), # 为每个 tool_ref 装 _echo reasoners_echo_reasoners(node),# 字符串 reasoner APP/EVAL_PLAN 控制器 subs_echo_subs(node), # SubStep 按 ref 装桩 agents_echo_agents(node), # APP 节点控制器装桩 gatelambda value: {approved: True, input: value}, modeEnforcementMode.DEV, # 开发模式强制 ) return env, projection五个桩收集器_echo_tools/_echo_reasoners/_echo_subs/_echo_agents各自遍历flow.tool_refs()或flow.walk()按toolref_key(ref)、reasoner 名、SubStep.ref等键装配_clear_frozen_hashes把CallStep.frozen_hash置为None确保同一 Flow 可以反复重跑而不被旧的哈希短路。这一步由既有测试tests/cli/test_run.py兜底runner.py瘦身为RunOutcome数据类加一个本地运行入口。四、Task 2子进程lint动作 父进程lint_agent薄封装4.1 先写失败的回归测试计划强调 TDD先在 tests/cli/test_pure_cli_regression.py 写出当前代码下必然失败的用例。fixturepure_module在临时目录里构造一个只含pureAgent 的 julep 工程pyproject.toml声明[tool.julep] src [pkg]。关键设计是 pure 主体必须echo 容忍它只读dict.keys()所以在julep run的{output: value}桩下不会因为无关的KeyError而误报def test_lint_resolves_pures_no_unknown_pure(pure_module: Path) - None: from julep.cli.config import load_config from julep.cli.lint import lint_agents cfg load_config(pure_module) findings, exit_code lint_agents(cfg, [triage], fail_severityerror) codes [f.code for f in findings] assert UNKNOWN_PURE not in codes, fpure not resolved by julep lint: {findings} assert RESOLVE not in codes, fresolve failed: {findings} assert exit_code 0修复前运行预期失败信息为UNKNOWN_PURE — arr function not registered: passthrough。4.2 子进程侧新增lintaction在 julep/cli/_resolve_child.py 的main()中、默认resolve尾部之前插入lint分支复用既有_discover_agent(root, src, target)找到 Flow然后在本进程内调用validate(result.found.to_ir())把每条诊断归一为{code, severity, message}三元组随 JSON 发出未找到 Agent 时发_not_found_error会附带 import 错误明细。落地版还叠加了queue_lane_diagnostics队列 lane 校验属于计划之后的自然演进。4.3 父进程侧统一子进程调用路径_invoke_childjulep/cli/resolve.py 的_invoke_child是所有子进程调用的唯一入口把{root, src, **extra}序列化成 JSONsubprocess.run([sys.executable, -m, julep.cli._resolve_child], inputarg, ...)执行然后从 stdout 里提取两个哨兵标记__JULEP_RESOLVE_BEGIN__/__JULEP_RESOLVE_END__之间的 payloadresolve.py 的_extract_payload对超时、非零退出、解析失败统一返回{error: ...}让所有调用方共享同一分支形状。重要细节payload 走stdin而非 argv。设计计划 Notes 里提示大输入会撞上 OS 单参数长度上限落地实现直接把整份请求写进 stdin 规避了MAX_ARG_STRLENLinux 约 128 KiB对应回归测试test_run_large_input_not_limited_by_argv用 30 万字符的输入验证了这一点tests/cli/test_pure_cli_regression.py。这是计划之外、落地时补上的一个真实增强。基于_invoke_child父进程新增两个冻结数据类与一个包装函数dataclass(frozenTrue) class LintResolution: diagnostics: list[dict[str, str]] error: str | None None def lint_agent(cfg, name, *, timeout30.0, env_varsNone, queuesNone, queue_envlocal) - LintResolution: Validate an agent IN the child (where pures are registered) and return diagnostics. data _invoke_child(cfg, {name: name, action: lint, env_vars: env_vars, queues: queues or {}, queue_env: queue_env}, timeouttimeout) if error in data: return LintResolution(diagnostics[], errorstr(data[error])) raw data.get(diagnostics, []) return LintResolution(diagnostics[dict(d) for d in raw], errorNone)原有的resolve_agent也被重构为复用_invoke_child行为不变子进程的默认action保持resolve因此ls/show/graph/deploy/status的 IR-only 路径不受影响。4.4 重接lint_agents的消费端julep/cli/lint.py 的lint_agents现在对代码 Agent 走lint_agent子进程内校验对配置的 ctx pipeline 走_lint_ctx_pipeline本进程结构化校验、不做 IO再统一按严重度闸门排序_SEVERITY_ORDER {info: 0, warning: 1, error: 2}任何等于或高于fail_severity的诊断都会把退出码置为1RESOLVE错误子进程传输层失败或 Agent 找不到直接返回退出码2。lintaction 的产出同时被cli.lint命令与相关子命令消费。五、Task 3子进程run动作 run_agent_local重新接线5.1 子进程侧新增runaction在 julep/cli/_resolve_child.py 的lint之后插入run分支_discover_agent找到 Flow →to_ir()→build_echo_env(node)这正是 Task 1 提取的产物→asyncio.run(interpret(node, payload.get(value), env))。成功与异常两条路径都回传value、events每个ProjectionEvent.to_json()序列化与error三件套if action run: import asyncio from julep.cli._echo import build_echo_env from julep.execution.interpreter import interpret result _discover_agent(root, src, target) if result.found is None: _emit({error: _not_found_error(target, result.import_errors), events: []}) return 0 node result.found.to_ir() env, projection build_echo_env(node) try: outcome asyncio.run(interpret(node, payload.get(value), env)) except Exception as exc: # 把运行失败序列化给父进程 _emit({value: None, events: [e.to_json() for e in projection.events()], error: str(exc)}) return 0 _emit({value: outcome.value, events: [e.to_json() for e in projection.events()], error: None}) return 05.2 父进程侧RunResolutionrun_agentjulep/cli/resolve.py 的run_agent把{name, action: run, value}交给_invoke_child。一个重要的分支细节传输层错误与运行错误必须区分——若 payload 里只有error而没有value说明子进程压根没跑起来超时/非零退出/解析失败此时没有 events 可用返回RunResolution(valueNone, events[], error...)若value存在则error字段是解释器运行失败信息events 依然有效可供 trace tree 渲染。5.3run_agent_local的签名演进计划中有一个刻意的两阶段设计Step 5 先用NotImplementedError桩暴露当前签名run_agent_local(resolved, value, run_id)缺少子进程所需的cfgStep 6 再改成最终签名run_agent_local(cfg, name, value, *, run_id)并同步更新cli.run的本地运行分支与测试调用点。当前仓库的最终形态julep/cli/runner.pydef run_agent_local(cfg, name, value, *, run_id, env_varsNone) - RunOutcome: Execute an agent locally by interpreting it in the resolve child (pures live). resolution: RunResolution run_agent(cfg, name, value, env_varsenv_vars) events [ProjectionEvent.from_json(e) for e in resolution.events] return RunOutcome(run_idrun_id, valueresolution.value, eventsevents, errorresolution.error)RunOutcome保持既有形状run_id/value/events/errorevents用ProjectionEvent.from_json重建因此 trace tree 渲染、save_run落盘等下游消费端完全无感。回归测试也同步更新为run_agent_local(cfg, triage, TICKET-42, run_idt-pure)并断言 pure 真的执行了结果里含seen键。5.4 运行时解析 pure 的依据子进程内interpret之所以能找到 pure是因为解释器julep/execution/interpreter.py在解析arr等节点时查的是当前进程的全局注册表——而这个进程正是导入过用户模块、执行过pure装饰器的子进程。这也是 julep/execution/harness.py 中UNKNOWN_PURE被当作可预期诊断处理的前提修复后真正注册过的 pure 一定可解析没注册的引用才是真错误。六、Task 4端到端 CLI 回归 文档澄清函数级测试证明 resolve 边界已修好端到端测试证明用户可感知的ca/julep命令本身工作并把它永久锁定def test_cli_lint_and_run_end_to_end(pure_module: Path) - None: base [sys.executable, -m, julep.cli.main] lint subprocess.run(base [lint, triage], cwdpure_module, capture_outputTrue, textTrue) assert lint.returncode 0, lint.stdout lint.stderr assert UNKNOWN_PURE not in (lint.stdout lint.stderr) run subprocess.run(base [run, triage, --input, TICKET-42], cwdpure_module, capture_outputTrue, textTrue) assert run.returncode 0, run.stdout run.stderr assert unknown pure not in (run.stdout run.stderr).lower() assert output: in run.stdout落地版入口为python -m julep.cli.main计划写作时为python -m composable_agents.ca.cli命令julep lint triage与julep run triage --input TICKET-42均须零退出、无UNKNOWN_PURE且run输出包含output:行。文档侧计划要求在 docs-site/content/docs/guides/using-the-cli.md 的 Inner loop — local 小节补一条澄清明确julep run与dry_run的区别julep run使用离线 echo 桩执行只为观察trace tree 与控制流不产生真实值需要真实本地输出时应使用deployment.dry_run(input, reasoners{...})配合假 reasoners。两条路径中已注册的pure函数都会真实执行。七、已知边界与后续演进计划明确划出的范围外计划在 Notes / known limitations 中主动标注了两个非本次范围的问题值得读者注意echo 语义是有损的。由于 tool/reasoner 桩只返回{output: input}一个按具体工具输出键取值如hit[queue]的 pure 在julep run下会KeyError——这是既有的 echo 行为不是 pure 注册表 bug。回归测试特意把 pure 写成只读dict.keys()正是为了隔离这两个问题。让julep run支持像dry_run那样注入 fake handler是独立增强不在 WS1 范围内。输入尺寸。run 的 value 要跨进程传递虽然落地版改走 stdin 大幅放宽了限制test_run_large_input_not_limited_by_argv已用 300KB 输入验证但极端超大输入仍可能受传输通道制约。验收范围。除了本计划的pure回归样例self-review 还要求手动跑一次原实验脚本returns_triage.pymulti-pure each的复合 Flow的julep lint作为收尾验收。八、落地状态对照从计划到当前仓库计划中的每个产出物都能在当前仓库找到对应实现可直接对照阅读计划产出原composable_agents.ca.*当前仓库位置_echo.pyecho 环境构建器julep/cli/_echo.py_resolve_child.py子进程lint/run动作julep/cli/_resolve_child.pyresolve.py的_invoke_child/LintResolution/RunResolution/lint_agent/run_agentjulep/cli/resolve.pyrunner.py的run_agent_local(cfg, name, value, *, run_id)julep/cli/runner.pylint.py消费子进程诊断julep/cli/lint.py回归测试函数级 端到端 大输入tests/cli/test_pure_cli_regression.pyUNKNOWN_PURE诊断码来源julep/validate.py、julep/diagnostics.py设计 spec根因 §4.2、Approach B §4.3docs/plans/2026-06-29-composable-agents-dx-improvements.md落地版相比计划还有两处自然演进lint动作叠加了队列 lane 诊断queue_lane_diagnosticsrun/lint均支持env_vars透传子进程在导入用户模块前先用julep.dotctx_yglu.set_default_env绑定 dotctx 环境见 julep/cli/_resolve_child.py此外 payload 从 argv 改为 stdin 传输附带解决了大输入问题。九、可复用的架构模式小结把依赖注册表的操作放进注册表所在进程凡是通过名字解析的实体pure、reasoner、subflow其校验与求值都应在导入过声明代码的进程内完成跨界搬运源码重注册会破坏基于源码哈希的确定性契约。子进程协议三件套root/src定位工程name定位 Agentaction选择动作父进程统一经_invoke_child分发stdout 用哨兵标记包裹 JSON payload任何用户print都不会污染解析。传输层错误与业务错误分离payload 中error与value是否并存决定了调用方能否拿到events——这是run_agent/lint_agent健壮性的关键。TDD 与 e2e 双重锁定函数级回归验证 resolve 边界端到端子进程回归验证用户可见命令二者缺一不可。赞分享AI AgentAgent 框架后端【免费下载链接】julepJulep — durable, composable AI agents. Flows that crash and resume, retry safely, and explain every step.项目地址https://gitcode.com/gh_mirrors/ju/julep点击查看免费下载相关推荐Julep 本地开发与测试模式全指南从 julep run 到 julep dev upJulep 本地开发与测试模式全指南从 julep run 到 julep dev up Julep 为本地开发提供了从零依赖的前台执行到单机持久化栈的完整模AI AgentAgent 框架后端如何快速上手SillyTavernAI角色扮演完整指南如何快速上手SillyTavernAI角色扮演完整指南 SillyTavern是一款功能强大的AI角色扮演工具和LLM前端专为喜欢创建虚拟角色和沉浸式对话的人工智能AI 应用交互助手前端julep CLI 完全参考选择语法、配置模型与全命令实战指南julep CLI 完全参考选择语法、配置模型与全命令实战指南 julep 是 Julep 面向开发者的命令行入口在模块根目录运行它即可发现源码中的 agAI AgentAgent 框架后端上一篇tunnelto终极指南5分钟实现本地服务全球访问的完整方案下一篇Spectre.Console完全入门Python Rich同款的.NET控制台UI库5分钟告别丑陋终端输出创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考