跳到主要内容

数据截至 (上游 commit fc74d079a18c)

核心概念与数据模型

30 秒导读: Julep v3 没有旧版那种"agents/tasks/sessions/docs 落在 PostgreSQL 九张表"的 领域模型——它是一个编译器型框架:核心数据结构是一棵可序列化的 IR Node 树,外加一组 给工具钉行为的契约、一份部署时冻结的工具清单,和运行期派生的投影/轨迹。读完本章 你应该说清:Node/Op/Shape/ToolRef/ToolContract/Reasoner/Agent/Session 分别是什么、 一份部署物里装了什么、一次 run 在存储里长什么样。

本章是后续所有章节的地基。执行流程(解释器怎么走 IR)留给 02; 怎么用 Python 写出这些结构留给 03;工具面冻结细节留给 04;会话留给 05。 全景见 index

旧版去哪了: v1 的 developer/agent/user/task/execution/transition 九张表、TypeSpec 生成的 autogen 模型、memory-store 迁移目录在主干全部移除(README.md:236)。想看旧模型请去 v1 分支;本章只讲 v3。


1. 先建立直觉:三组"名词"

v3 的名词可以分成三组,分别回答三个问题:

  • "流程长什么样" —— Handle/Graph(编写期)、Node/Op(IR)、Shape(分级)。流程是数据, 不是代码路径。
  • "边界上有什么" —— ToolRef(引用一个工具)、ToolContract(工具的行为断言)、 ToolManifest(冻结的工具清单)、Reasoner(一次模型调用的声明)。这些是流程接触外界的叶子
  • "跑起来产生什么" —— Run、ProjectionEvent(pomset)、Trajectory(跨 run 轨迹)。 全部派生,不是运行必需。
名词类比一句话
Handle电路里的探针@flow 里的数据占位符,定义期流动、运行期才取值
Node(IR)编译后的 AST一棵 11 种算子组成的有限树,全系统唯一共识
Shape汽车排量分级流程"拥有续体"的成本等级,组合取最贵
ToolRef函数指针指向 native 工具(名)或 MCP 工具(server/tool)
ToolContract药品说明书工具的 effect(读了还是写了世界)+ 幂等性断言
ToolManifest装箱单部署时冻结的全部工具及其内容哈希
Reasoner点单模板一次模型调用:用哪个模型、系统提示、回复 schema
Agent正式员工有界控制器循环,逐轮从封闭决策集里选动作
Session电话线把有限流程包进无限 LOOP 的会话边界
ProjectionEvent行车记录仪帧一次节点激活的 Planned/Did/Failed 事实

本章最重要的一刀: 流程的定义(Node 树)与流程的执行(run/投影)彻底分离。 定义是不可变、可哈希的部署物;执行是派生、可重放的观测。这一刀贯穿 v3 所有设计。


2. 顶层关系图(谁编译成谁、谁派生谁)

怎么读这张图: 上半部是编译期(编写 → 图 → IR → 冻结部署物),箭头是"编译/填充"; 下半部是运行期(部署物 → run → 派生观测),箭头是"执行/派生"。

编写期 编译期(一次)
───────── ──────────────────────────────
@flow 函数 ──定义期执行──▶ dag.Graph(StepNode 单赋值步骤)
│ @tool/@pure/Reasoner │ compile(julep/dag.py:306)
│ 注册进 Registry ▼
│ Node 树(IR,julep/ir.py:425)
│ │ freeze(julep/freeze.py:504)
│ │ + ToolContract → 内容哈希
│ ▼
│ Deployment = 冻结 IR + ToolManifest + 诊断
──────────────────────────────────────────────────────────
运行期(每次)
Deployment ──start──▶ Run
│ 每节点激活

ProjectionEvent(Planned/Did/Failed) ← 单 run 观测
│ 效果层捕获

TrajectoryRun/Step/Value ← 跨 run 轨迹

三样东西的"身份"都是内容寻址的:工具按 definition_hash 哈希(julep/contracts.py:148), 部署物按 artifact_hash(julep/deploy.py:534),投影值按 value_ref 内容哈希 (julep/projection.py:106 ValueStore)。同内容即同身份,这是"冻结"的物理基础。


3. IR:一棵 11 种算子的 Node 树

3.1 Op:全部结构算子

Node(julep/ir.py:425)的 op 字段取自 Op 枚举(julep/kinds.py:14),总共 11 种:

Op是什么关键子字段
PRIM叶子:调工具/模型/子流程step(CallStep/ThinkStep/SubStep)
IDENT恒等(透传)——
ARR套一个具名纯函数pureargs
SEQ顺序:左输出接右输入leftright
PAR并行:两分支同输入,按 Merge 汇合leftrightmerge
EACH遍历列表逐项跑 bodybodybound(并行度)、reducer
ALT分支:谓词二选一,或 select 多路pureselect+cases
ITER_UP_TO有界反馈:最多 bound 轮,收敛即停bodyboundpure(收敛判据)
EVAL_PLAN运行期编译计划再执行plancontroller
APPagent 控制器循环controllertoolsbudgetmax_rounds
LOOP无限会话循环(带通道)bodychannelsstate_schema

三点设计纪律:

  • 树永远有限。 递归只可能通过 ITER_UP_TO/EVAL_PLAN/APP 进入"被求值的程序", IR 树本身有限——这让 surface_shape(julep/shapes.py:102)可判定(julep/ir.py:1 模块 docstring)。
  • 线格式统一。 序列化用 camelCase 键,Python 与 TypeScript 前端产出同一份 JSON; 内容哈希用 canonical JSON(排序键、无空白,julep/ir.py:643 canonical_json)。
  • 叶子只有三种 step:CallStep(julep/ir.py:254,带可空 frozen_hash)、 ThinkStep(julep/ir.py:271,按名引用 Reasoner)、SubStep(julep/ir.py:311, 带一份 SubContract)。

3.2 三种保留的"伪工具"

IR 里预留了四个保留 native 工具名,解释器见到它们不发 HTTP,而是翻译成引擎原语 (julep/execution/interpreter.py:390 _eval_prim):

保留名常量被翻译成
__human_gate__HUMAN_GATE_TOOL(julep/ir.py:28)信号等待(等人审批,可带超时)
__sleep__SLEEP_TOOL(julep/ir.py:33)持久定时器(Temporal timer / DBOS.sleep)
__recv__RECV_TOOL(julep/ir.py:37)通道阻塞接收
__emit__EMIT_TOOL(julep/ir.py:41)通道顺序输出

这样"等人""睡觉""收发消息"和普通工具调用在 IR 里形状一致,组合子(如 human_gate(),julep/derived.py:221)可以像包工具一样包它们。

3.3 每节点注解 Ann 与上下文策略

Ann(julep/ir.py:177)是可选的每节点标注:成本/风险提示、缓存键、effect、超时、 重试(次数/间隔/退避)、可批处理。ContextPolicy(julep/ir.py:129)声明这个叶子读多少 会话上下文——scope 取四档 ContextScope(julep/kinds.py:106): none/local/summary/whole_session上下文永远显式声明,绝不隐式 (par 里两个 whole_session 读会被校验器降级为串行,julep/ir.py:129 的 docstring)。


4. Shape 形态格:流程的"成本等级"

Shape(julep/kinds.py:30)给每个流程分六级,顺序即"拥有续体(continuation)的成本":

Shape白话
0Pipeline直线流水,干完就完
1Dataflow有并行分支
2Branching有条件分支
3Feedback有反馈循环(有界收敛)
4Staged分阶段(计划先行、逐段准入)
5Agent开放式循环,自己决定何时结束

组合规则是半格 join:shape_join(*shapes)(julep/kinds.py:81)取最贵者—— "一条 Pipeline 和一个 Agent 组成的 seq 是 Agent"。surface_shape(julep/shapes.py:102) 从 IR 树推导形态,不需要用户声明。

Shape 不只是分类学,它有三处实际用途:

  • SubContract 防火墙:子流程在边界上只暴露一个 shape 合同(julep/ir.py:284 SubContract,含 SummaryPolicy),内部多贵不泄漏。
  • 投影成本汇总:投影事件带 shape,cost_by_shape 按它聚账(julep/execution/harness.pyFlowWorkflow.projection 查询)。
  • Reasoner 降维:Reasoner.max_rounds 有界 → 降成 ITER_UP_TO(Feedback); agent: true 开放 → APP(Agent)——"轮数上限成为形态",见 julep/dotctx.py:1 模块 docstring。

5. 工具侧:引用、契约、清单

5.1 ToolRef:未绑的引用

IR 里一个 call 节点只带 ToolRef:NativeTool(name)(自家 HTTP 工具)或 McpTool(server, tool)(来自 MCP 服务器),见 julep/ir.py:61julep/ir.py:72toolref_key(julep/ir.py:94)给它们一个稳定的可读标识(native 用名字,MCP 用 server/tool)。此时的引用是未绑的——谁也不知道这个工具现在长什么样。

5.2 ToolContract:行为断言

ToolContract(julep/contracts.py:22)只有两个字段,却决定重试与竞速两大政策:

  • effect:这个工具对世界做了什么。四档 Effect(julep/kinds.py:88): read/write/external/dangerous
  • idempotency:重复调用安全吗。四档 Idempotency(julep/kinds.py:97): required(调用方保证幂等键被尊重)/native(天然幂等)/best_effort/none

关键立场:MCP 的注释性提示(hint)按规范不可信contract_from_annotations (julep/contracts.py:111)把没明说的全部塌缩成保守默认 CONSERVATIVE_DEFAULT (julep/contracts.py:40)= write + none——没被断言的工具不能盲目重试、不能进 race。 唯一受信任的断言来源是人写的 capability manifest(§5.4)。

5.3 ToolManifest 与 freeze

freeze(julep/freeze.py:504)是纯函数:快照进、冻结 IR + 清单出。它把每个引用的工具 解析成 FrozenTool(julep/contracts.py:201,含 definition_hash),并把哈希回填到每个 CallStep.frozen_hash(julep/freeze.py:620bind 在运行期按哈希查回)。 快照有两个来源:McpToolSpec(julep/freeze.py:70,来自 tools/list)与 NativeToolSpec(julep/freeze.py:89,本机注册的 schema)。冻结后:

  • 工具面永不漂移——运行期任何工具 schema 变化都可检出(§5.4 的 preflight);
  • 重放可校验——manifest 哈希对得上才认。

5.4 CapabilityManifest:人写的允许清单

CapabilityManifest(julep/capabilities.py:85)回答"这个部署允许碰什么":哪些工具 (含断言的 effect/幂等覆盖)、哪些 reasoner、哪些上下文作用域、哪些 MCP 服务器、总预算 Budget(julep/capabilities.py:49:cost/tokens/wall_seconds)。语义是 present 即 deny-by-default:没写 reasoners 段 = 任意 reasoner 都行;列了两个 = 第三个 被拒(julep/capabilities.py:1 模块 docstring)。它在三个缝上被执行:编译(引用必须被授予)、 调度(预算)、运行(网络出网 + 模型允许清单)。


6. 三种"活的"概念:Reasoner、Agent、Session

6.1 Reasoner:一次模型调用的声明

Reasoner(julep/dotctx.py:172)是一个 frozen dataclass:名字、模型 slug、系统提示、 回复 schema(reply= 从 TypedDict/Pydantic 推导,julep/dotctx.py:131 _typeddict_to_schema)、 温度、轮数上限、上下文作用域、渲染器、skills 键等。它按名注册进 Registry(和纯函数同一机制, julep/registry.py:662 DEFAULT_REGISTRY),IR 里的 ThinkStep 只存名字。

"轮数上限成为形态"是 v3 的一个漂亮映射(julep/dotctx.py 模块 docstring):

Reasoner 配置降成的 IR
有界 max_rounds >= 1ITER_UP_TO(Feedback)
开放(agent: true/无上限)APP(Agent)
标记 sub:SubStep(子工作流,Joined 防火墙)
其余单轮单个 think 叶子(Pipeline)

6.2 Agent:有界控制器循环

Agent(julep/agent.py:446)是 Shape 格的顶点:一个逐轮决策的循环。它之所以不失控, 靠两个约束(julep/agent_loop.py:1 模块 docstring):

  • 封闭决策集:控制器每轮只能返回 Decision(julep/agent_loop.py:97)之一—— finish/escalate/call(调一个已授予工具)/call_many/sub(调一个预注册子流程)/ controller_error。不能现场生成 IR。
  • 预算守卫 + 历史截断:每轮扣估算成本,超了停(julep/agent_loop.py:585 would_exceed_budget);轮数过阈值就 continue-as-new 截断历史 (julep/agent_loop.py:592 should_continue_as_new)。

AGENT_REPLY_SCHEMA(julep/agent.py:60)就是控制器回复的 JSON Schema:一个 tool/sub/finish 的小封闭集。配套还有"计划提取"——把观察到的动作轨迹宏录制为候选 Plan (generalize_trace_to_plan,julep/agent_loop.py:825),过 §8 准入后升级成可重放的 stage(promote_plan,julep/agent_loop.py:871)。

6.3 Session:无限 LOOP 边界

Session(julep/session.py:75)把一个有限的"单轮流程"包进一个无界的 Op.LOOP: 外面喂消息(anamorphism),里面每轮是同一个有限解释(cata inside),Channel (julep/session.py:43)是类型化的输入/输出端口。@session 装饰器(julep/session.py:187) 和 scan(julep/session.py:161)是两个使用入口。会话的 durable 形态(Temporal SessionWorkflow + Postgres session store)见 05


7. 运行期数据模型:runs、投影、轨迹

v3 没有"业务对象表",运行期持久化只有两个派生面 + 一张 run 状态表:

7.1 runs 表(控制平面的真相)

julep/execution/projection_sql.py:22runs 表是控制平面看到的 run 状态机: status ∈ {submitting, accepted, start_failed, running, completed, failed, canceled, terminated} (状态前置关系见 julep/execution/projection_store.py:44 _RUN_STATUS_PREDECESSORS)。 输入/结果是 input_ref/result_ref(内容引用),不内联大值。

7.2 投影事件(pomset)

ProjectionEvent(julep/projection.py:44)是不可变的"一个事实":Planned/Did/Failed 三型(EventType,julep/projection.py:37),核心字段是 node(IR 节点 id)、cid (激活 id——同一节点可多次激活)、causes(上游事件 id = IR 输入边,正是它把日志变成 偏序集 pomset)、value_ref(Did 事件的内容哈希,大值进 ValueStore 只存一次)。 落库 schema 在 julep/execution/projection_sql.py:51(projection_events)与 :77(projection_values)。

7.3 轨迹(跨 run 因果历史)

trajectory.py 模块开头的对照表(julep/trajectory.py:14 起)说得清楚:

投影面轨迹面
范围一个 run/段根 run + 全部子 run
内容IR 激活 pomset缝合的因果历史
用途观测/调试可导出的训练样本
进 ValueStore 的引用进 BlobStore 的引用

数据结构是 TrajectoryRun(julep/trajectory.py:243)、TrajectoryStep(:300)、 TrajectoryValue(:363);Postgres 表定义在 julep/execution/trajectory_sql.py:13 (trajectory_runs)、:31(steps)、:51(values)。捕获是尽力而为的,绝不允许影响确定性 执行(julep/trajectory.py 模块 docstring)。跨 continue_as_new 段靠 root_run_id + segment_seq 缝合。


8. 同一概念的三副面孔(编写 → 图 → IR)

和 v1 的"API 模型/spec/落库"三面孔不同,v3 的同一逻辑有编写期与线格式两副面孔, 中间隔着一张单赋值图:

你写的中间表示(dag)IR(线格式)
lookup_ticket(h, retries=2)StepNode(kind=TOOL, ref=..., ann=...)Node(op=PRIM, step=CallStep)
ticket_prompt(h)(注册 @pure)StepNode(kind=PURE)Node(op=PRIM→ARR, pure=名)
think(r, h)StepNode(kind=THINK)CallStep→ThinkStep(reasoner=名)
h | h2两个输入边汇进一步std.merge 纯函数
h["key"]输入投影std.pluck 纯函数
cond(p, h, then=..., orelse=...)StepNode(kind=COND, if_true/if_false)ALT(pure=p)
each(body, items)StepNode(kind=EACH, body=Graph)EACH(body, bound=并行度)
  • Handle(julep/define.py:383)是编写期的占位符;Graph/StepNode(julep/dag.py:90/ julep/dag.py:61)是单赋值中间图,compile(julep/dag.py:306)拓扑排序后产出 IR。
  • 命名即身份:步骤名从整个函数的 AST 源码推导(单赋值),name= 是显式逃生门 (julep/define.py:1 模块 docstring)。

9. 边界与容易踩的点

  • IR 是唯一共识。 harness、校验器、投影、分析器只认 Node 树(julep/ir.py:1 docstring); 改任何行为都要过"是否改变了 IR"这一问。
  • 契约默认保守。 未断言的 MCP 工具 = write + none:不能重试、不能 race。要解锁必须写 capability manifest(julep/contracts.py:1 docstring),没有"信任一切"开关。
  • 冻结时机是个显式的缝。 deploy 默认 deploy_time(一次冻结、次次复用);工具面会漂移的 场景用 per_run + Deployment.refresh(julep/deploy.py:615)。
  • 投影不是持久化机制。 Temporal 历史才是;投影丢了可以重放重导出(julep/projection.py:1 docstring)。别把投影当审计的唯一副本。
  • 大值不进事件。 Did 事件只带 value_ref;值本体在 ValueStore/BlobStore,按内容哈希 去重(julep/projection.py:106)。
  • 没有"用户/租户"领域对象了。 多租户身份是 RunPrincipal(julep/execution/effects.py:68) ——一个不透明的 dict 引用,由 dispatch 注入、穿透进每个效果载荷,框架不解释它 (且绝不允许装秘密值,同处注释)。

10. 代码地图(导航索引)

主题文件路径符号名
IR 节点julep/ir.py:425Node / Node.to_json / Node.walk
结构算子julep/kinds.py:14Op
形态格julep/kinds.py:30Shape / shape_join
形态推导julep/shapes.py:102surface_shape
保留工具名julep/ir.py:28HUMAN_GATE_TOOL / SLEEP_TOOL / RECV_TOOL / EMIT_TOOL
叶子 stepjulep/ir.py:254CallStep / ThinkStep(:271) / SubStep(:311)
子流程合同julep/ir.py:284SubContract / SummaryPolicy(julep/kinds.py:115)
节点注解/上下文julep/ir.py:177Ann / ContextPolicy(:129)
工具引用julep/ir.py:61NativeTool / McpTool / toolref_key(:94)
工具契约julep/contracts.py:22ToolContract / CONSERVATIVE_DEFAULT(:40)
契约哈希julep/contracts.py:148definition_hash / FrozenTool(:201)
冻结julep/freeze.py:504freeze / McpToolSpec(:70) / bind(:620)
能力清单julep/capabilities.py:85CapabilityManifest / Budget(:49) / ToolGrant(:63)
编写期 Handlejulep/define.py:383Handle / FlowDef(:475)
中间图julep/dag.py:61StepNode / Graph / compile(:306)
Reasonerjulep/dotctx.py:172Reasoner / reasoner_from_settings(:475)
Agent 门面julep/agent.py:446Agent / AGENT_REPLY_SCHEMA(:60) / tool(:354)
控制器循环julep/agent_loop.py:97Decision / AgentState(:465) / should_continue_as_new(:592)
计划提取julep/agent_loop.py:825generalize_trace_to_plan / extract_plan(:854)
会话julep/session.py:75Session / Channel(:43) / session(:187)
投影事件julep/projection.py:44ProjectionEvent / EventType(:37) / ValueStore(:106)
runs 表julep/execution/projection_sql.py:22CREATE TABLE runs
轨迹julep/trajectory.py:243TrajectoryRun / TrajectoryStep(:300) / TrajectoryValue(:363)
轨迹表julep/execution/trajectory_sql.py:13CREATE TABLE trajectory_runs

下一步: 这些静态结构如何"活"起来——interpret() 怎么走树、Temporal 怎么把效果变 durable、契约怎么驱动重试——请读 02 任务执行引擎