Recipe Development Guide
This document is a complete development reference for the Recipe subsystem. A Recipe is Familiars' "single-skill automation graph": a DAG runtime where LLM calls and tool executions are nodes, the variable pool is the state, and conditional edges are branches.
1. Concept Model
1.1 What is a Recipe
Recipe maps one "turn of conversation" of an agent to a state graph:
- Entry:
startnode (the single global entry anchor) - Exit:
donenode (the single global exit), must bindoutput/finish_reason - In between:
compute/llm_call/tool_call/rag_query/emit_event/for_eachnodes
Each node reads its inputs from the global variable pool and writes outputs back to it; an edge's condition= is a Python boolean expression; multiple outgoing edges can branch concurrently (the host round-robins across active branches).
1.2 Relationship to the Legacy Pipeline
The legacy manifest.chains + hooks linear chain is a special-case graph where every edge condition is "True". src/recipes/graph/translator.py automatically compiles a legacy manifest into graph.json (cached as _auto_graph.json); once the user saves in the editor it is written as the official graph.json, and from then on the manifest's chains/hooks fields are no longer read.
2. Directory and Files
2.1 Source Tree
src/recipes/
├── __init__.py
├── manifest.py # RecipeManifest 模型(输入/输出/工具/知识库/客户端/RAG/事件类型/渲染器)
├── store.py # RecipeStore — 磁盘 layout + CRUD
├── context.py # RecipeContext — bind 时上下文
├── runtime.py # Graph 运行期入口
├── catalog.py # 工具目录构建(_build_tools_catalog)
├── chain_catalog.py # 旧链式目录
├── approvals.py # 能力审批
├── audit.py # 审计
├── eval.py # 评估
├── filters.py # 过滤器
├── folder_protocol.py # 文件夹协议
├── heartbeat.py # 心跳 cron
├── marketplace.py # marketplace 索引
├── migrations.py # 迁移
├── packaging_cli.py # CLI 打包
├── reasoning.py # 推理
├── renderer_compiler.py # .vue 渲染器预编译
├── runtime_flags.py # 运行时 flag
├── source_reader.py # 源码读取
├── authoring/ # Copilot 辅助创作
│ ├── orchestrator.py
│ ├── prompts.py
│ └── state.py
├── graph/ # ★ 状态图核心
│ ├── schema.py # RecipeGraph / GraphNode / GraphEdge / GraphVariable / SubGraphTemplate / SubgraphIO
│ ├── validate.py # validate_graph — 20+ 条静态校验
│ ├── runner.py # 状态图执行器(_dispatch_chain_slot, _chain_node, _expand_for_each_in_scope)
│ └── translator.py # manifest chains/hooks → RecipeGraph
└── sandbox/ # 沙箱执行
├── ast_guard.py # AST 白名单(permanently denied / requestable)
├── host.py # 沙箱宿主
├── snippet.py # call_snippet — 把代码包成 def _node_<uuid>(**inputs): ...
└── worker.py # 沙箱 worker2.2 Runtime Disk Layout
Each recipe lives under workspace/agent_recipes/<id>/:
workspace/agent_recipes/<id>/
├── manifest.json # RecipeManifest
├── user_code/
│ ├── graph.json # RecipeGraph(核心,前端编辑器的源真相)
│ ├── *.py # 自定义 compute / args_builder / result_parser 代码
│ └── tools/ # manifest.tools.custom_tools 的 .py 实现
│ └── <tool_name>.py # 顶层函数名必须 == tool key
├── knowledge/ # manifest.knowledge.identity/agents_md/user_md 指向的 md 文件
│ ├── IDENTITY.md
│ ├── AGENTS.md # ★ recipe SKILL 通过 execute_skill 进入 flow,必须存在
│ └── USER.md
├── external_kb/<kb_name>/ # manifest.knowledge.external_kbs 对应的文件目录
├── skills/ # manifest.skills.custom_templates 的 markdown
├── renderers/ # manifest.event_types[].renderer 与 output_renderers[].source_file
│ └── *.vue
└── recipes/ # 归档的 recipe(嵌套)2.3 Built-in Recipes
Shipped with the repository, auto-registered by install_built_ins(), listed under the "System" group in the frontend, not editable/deletable:
| ID | Path | Description |
|---|---|---|
hello_world | src/recipes/_builtin/hello_world/ | Minimal graph recipe — one compute node echoes the user input in reverse and then goes done |
default_recipe | src/recipes/_builtin/default_recipe/ | Default recipe — a full state-graph mirror of the generate() main pipeline (sensitive → triage → RAG → planner → executor → synthesizer → verifier → replanner) |
recipe_author_agent | src/recipes/_builtin/recipe_author_agent/ | Recipe-authoring assistant agent |
3. Schema Reference
3.1 RecipeGraph (graph.json Top Level)
class RecipeGraph(BaseModel):
version: int = 1
variables: list[GraphVariable] # 全局变量池
nodes: list[GraphNode] # 节点集合
edges: list[GraphEdge] # 边缘集合
subgraphs: list[SubGraphTemplate] = [] # for_each 引用的子图模板(legacy)
entry: str = "" # legacy 单入口(迁移到 entries)
entries: list[str] = [] # 多入口(START 节点存在时被强制为 [start.id])
exits: list[str] = [] # 默认 = 所有 done 节点3.2 GraphVariable
class GraphVariable(BaseModel):
name: str # identifier-like(alnum + underscore)
type: str = "any" # str|int|float|bool|list|dict|any
# + 别名:string|number|integer|boolean|array|object → 规范化
external: bool = False # True = host 在 turn 开始注入(只读)
desc: str = ""Default external variables (injected automatically by the host each turn; the recipe need not declare them):
| Name | Type | Description |
|---|---|---|
query | str | User's raw input for this turn |
history | list | List of historical dialogue messages |
attachments | list | User's attachment list for this turn |
agent_id | int | Current agent id |
dialogue_id | int | Current dialogue id |
3.3 GraphNode
class GraphNode(BaseModel):
id: str # identifier-like
kind: str = "compute" # start|compute|llm_call|tool_call|rag_query|emit_event|for_each|done
inputs: list[str] = [] # 从变量池读
outputs: list[str] = [] # 写回变量池
code: str = "" # compute 节点用(snippet:return {outputs})
args_builder: str = "" # side-effect 节点用(snippet,构造 action args)
result_parser: str = "" # side-effect 节点用(snippet,解析 observation)
desc: str = ""
mode: str = "custom" # custom | native
config: dict = {} # native 模式的结构化配置
render_as: dict[str, str] = {} # done 节点专属:metadata key → 渲染 hint(markdown|image|json|text|raw)
position_x: float = 0.0 # UI 位置
position_y: float = 0.0
# for_each 内联 body(2026-08-07 redesign,优先于 config.body_template)
body_nodes: Optional[list[GraphNode]] = None
body_edges: Optional[list[GraphEdge]] = None
body_entry: str = ""
body_exit: str = ""
body_inputs: Optional[list[SubgraphIO]] = None
body_outputs: Optional[list[SubgraphIO]] = NoneNode kind Semantics
| Kind | Meaning | Required fields (custom mode) | Required fields (native mode) |
|---|---|---|---|
start | Global entry anchor | None (pure anchor; the schema auto-clears all fields) | Same |
compute | Pure local Python, no side effects | code | Same |
llm_call | Host calls the LLM | args_builder + result_parser | config.system_prompt / user_prompt_template / output_format |
tool_call | Host calls a tool | args_builder (returns {tool_name, arguments}) + result_parser | config.tool_name + config.argument_map |
rag_query | Host queries the KB | args_builder + result_parser | config.source + config.query_input |
emit_event | Host pushes an event to the frontend (can block waiting for a user response) | args_builder | config.event_type + config.field_values |
for_each | Expanded N times at runtime (dynamic loop) | None (relies on config) | Same |
done | Terminates a branch | args_builder (returns {output, finish_reason}) | Same |
3.4 GraphEdge
class GraphEdge(BaseModel):
source: str # 起节点 id
target: str # 终节点 id
condition: str = "True" # Python bool 表达式(AST 白名单求值)
directed: bool = True # False = 双向,任一端完成都触发另一端
source_handle: str = "" # UI: VueFlow handle id
target_handle: str = ""
join_id: str = "" # AND-edge 分组(相同 join_id + 相同 target = 所有 source 都触发才跑 target)3.5 Native Mode Config Schema
When mode="native", the runner calls host actions directly (ConversationSession / ToolDispatcher / Memorizer) instead of running a Python snippet.
llm_call native config
{
"system_prompt": str | None, # 内联系统提示
"system_prompt_file": "" | "AGENTS.md" | "IDENTITY.md", # 从 knowledge 文件加载
"user_prompt_template": str, # 支持 {{var}} 模板
"output_format": {
"type": "str" | "markdown" | "json" | "schema" | "schema_ref" | "schema_from_var",
"schema": {...}, # type=schema 时的 JSON Schema
"ref": "plan_spec.json", # type=schema_ref 时的文件名
"schema_var": "my_schema", # type=schema_from_var 时的变量名
"on_parse_error": "soft" | "hard"
},
"output_var": str, # 把 parsed dict 包到一个变量名下(防止字段散到顶层)
"output_map": {ret_field: var_name}, # 把 LLM 输出字段映射到变量名
"tool_whitelist": [str], # 本节点允许调用的工具
"loop_mode": "single" | "tool_loop", # tool_loop 时 LLM 多轮发 tool_call
"max_turns": int, # tool_loop 上限
"history_mode": "none" | "inherit" | "custom",
"client_id": str # 指定 client(覆盖 manifest default)
}tool_call native config
{
"tool_name": str, # 必须存在且可通过 ToolDispatcher 调用
"argument_map": {param: var_name | "literal_value"} # 字面量用非 identifier 字符串
}rag_query native config
{
"source": "memorizer" | "external_kb" | "skills_registry" | "data_registry" | "dialog_history",
"query_input": str, # 必须 in node.inputs
"kb_names": [str], # source=external_kb 时必须,且要在 manifest.knowledge.external_kbs 里
"top_k": int,
"result_map": {field: var_name}
}emit_event native config
{
"event_type": str, # 内建:__log__ | __output__;或 manifest.event_types 里声明的
"field_values": {field_name: value | "=expr"}, # =expr 在运行时对变量池求值
"mode": "custom" | "native" # custom 时会忽略 native config
}3.6 for_each Node Config
{
"iterable_var": str, # 必须 in graph.variables(要迭代的变量)
"iter_var": str, # 每次迭代绑定的当前项名字(不能 == iterable_var)
"body_template": str, # 引用 SubGraphTemplate(与 inline body 二选一)
# --- 或者 inline body(2026-08-07 设计)---
# node.body_nodes / body_edges / body_entry / body_exit / body_inputs / body_outputs
"mode": "parallel" | "serial", # 默认 parallel
"output_var": str, # 迭代结果收集到哪个变量(必须 in node.outputs)
"reduce_strategy": "list" | "concat" | "merge_dict" | "last",
"result_var": str, # 每次迭代里要收集的变量(merge_dict 时必设)
"input_bindings": {sub_input: parent_var},
"output_bindings": {sub_output: parent_var}
}3.7 RecipeManifest (manifest.json Top Level)
Key fields (the pydantic model lives in src/recipes/manifest.py):
class RecipeManifest(BaseModel):
id: str # 全局唯一,== 目录名
name: str
description: str = ""
version: str = "1.0.0"
author: str = ""
tags: list[str] = []
# 输入输出契约
input_vars: list[RecipeVar] # RecipeVar: name, type, description, required
output_vars: list[RecipeVar]
# 工具
tools: RecipeTools = {
"whitelist": None | [str], # None = 全部;[str] = 只这些
"per_tool_overrides": {tool: file.py},
"custom_tools": {tool: CustomToolDef},
"categories": [ToolCategoryDef],
"tool_categories": {tool: category}
}
# RAG
rag: RecipeRAG = {
"mode": "builtin" | "custom",
"alpha": float, "beta": float, "gamma": float,
"top_k": int, "sliding_window_size": int,
"user_code": str,
"hybrid_sparse_weight": float, "rerank_disabled": bool,
"graph_expand_top": int, "graph_expand_hops": int
}
# 技能
skills: RecipeSkills = {"enabled": [], "custom_templates": []}
# 知识
knowledge: RecipeKnowledge = {
"identity": str, "agents_md": str, "user_md": str,
"external_kbs": [ExternalKB],
"kb_categories": [KbCategoryDef]
}
# 生成管线
generate: RecipeGenerate = {
"image": RecipeGenerateImage,
"report": RecipeGenerateReport,
"digital_human": RecipeGenerateDigitalHuman
}
# 客户端覆盖
clients: RecipeClients = {
"existing": {client_id: ExistingClientOverride},
"custom": [CustomClient]
}
# 自定义事件类型
event_types: list[RecipeEventType]
# 输出渲染器
output_renderers: list[RecipeOutputRenderer]
# 能力
capabilities: {"extra_modules": [str]}
# 执行
execution: {"parallel_mode": "topo" | "..."}
# 历史管理
history: RecipeHistory = {"mode": "builtin" | "custom", "filename": str}
# 长期记忆
memorizer: RecipeMemorizer = {"auto_write": bool}4. REST API
4.1 Recipe CRUD (/api/agent_recipes)
| Method | Path | Description |
|---|---|---|
| GET | /api/agent_recipes | List all recipes |
| GET | /api/agent_recipes/{id} | Fetch one |
| POST | /api/agent_recipes | Create |
| PUT | /api/agent_recipes/{id} | Update manifest |
| DELETE | /api/agent_recipes/{id} | Delete |
| POST | /api/agent_recipes/{id}/duplicate | Duplicate |
| POST | /api/agent_recipes/{id}/set_default | Set as default for an agent |
| POST | /api/agent_recipes/{id}/create_agent | Create an agent bound to this recipe |
4.2 Docs & Utility Files (/api/agent_recipes/{id}/...)
| Method | Path | Description |
|---|---|---|
| GET | /{id}/docs | List files in the knowledge directory |
| GET | /{id}/docs/{name} | Read a file |
| PUT | /{id}/docs/{name} | Write a file |
| DELETE | /{id}/docs/{name} | Delete |
| GET | /{id}/utils | List tool .py files under user_code/ |
| GET | /{id}/utils/{name} | Read tool source |
| PUT | /{id}/utils/{name} | Write tool source |
| POST | /{id}/utils/upload | Multipart upload a tool .py |
| GET | /{id}/renderers | List renderers |
| GET | /{id}/renderers/{name} | Read a renderer's .vue source |
4.3 Import / Export
| Method | Path | Description |
|---|---|---|
| GET | /{id}/export_zip | Export as .zip |
| POST | /import_zip | Import from .zip |
| POST | /preview_zip | Preview .zip contents |
| POST | /import_folder | Import from a directory |
4.4 Development & Publishing (/api/recipe_dev)
| Method | Path | Description |
|---|---|---|
| POST | /api/recipe_dev/compile | Compile validation (structural) |
| POST | /api/recipe_dev/dryrun | Dry run (no real model; uses a stub asker) |
| POST | /api/recipe_dev/e2e_test | End-to-end test |
| POST | /api/recipe_dev/publish_relay | Publish to the relay marketplace |
| GET | /api/recipe_dev/review_status | View review status |
| GET | /api/recipe_dev/catalog | Tool catalog |
| GET | /api/recipe_dev/_meta/protocol | Protocol metadata |
4.5 State Graph Read/Write
Saving actually goes through PUT /api/agent_recipes/{id} with a merged manifest + graph.json payload; the frontend editor POSTs the edited graph directly.
5. Validator (validate_graph)
src/recipes/graph/validate.py:validate_graph(graph, manifest, agent_id) returns a list of violations: {node_id?, field?, code, message, severity}. A severity of "error" blocks saving; "warning" only shows a hint.
5.1 Validation Checklist
| Code | Severity | Meaning |
|---|---|---|
NO_DONE | error | No done node |
MULTIPLE_DONE | error | Multiple done nodes |
NO_START | warning | No start node (legacy mode still works) |
MULTIPLE_START | error | Multiple start nodes |
START_HAS_INCOMING_EDGE | error | The start node has an incoming edge |
START_NOT_SOLE_ENTRY | error | entries ≠ [start.id] |
START_IS_EXIT | error | start is also an exit |
JOIN_UNDIRECTED | error | A join_id member is an undirected edge |
JOIN_MULTIPLE_TARGETS | error | The same join_id points to multiple targets |
JOIN_DANGLING | error | A join_id member references a nonexistent node |
JOIN_SINGLE_MEMBER | warning | A join_id group has only one edge |
ORPHAN_SUBGRAPH | warning | A SubGraphTemplate is not referenced by any for_each |
DUP_SUBGRAPH_ID | error | Duplicate subgraph id |
BODY_ENTRY_UNREACHABLE | warning | The subgraph's body_entry has no incoming edge |
FE_NO_ITER_VAR | error | for_each is missing iter_var |
FE_NO_ITERABLE | error | for_each is missing iterable_var |
FE_ITERABLE_NOT_DECLARED | error | iterable_var is not in variables |
FE_ITER_VAR_COLLISION | error | iter_var == iterable_var |
FE_NO_BODY | error | Neither body_template nor an inline body |
FE_BODY_NOT_FOUND | error | body_template is not in subgraphs |
FE_BAD_MODE | error | mode is not parallel/serial |
FE_BAD_REDUCE | error | Invalid reduce_strategy |
FE_OUTPUT_NOT_DECLARED | error | output_var is not in variables |
FE_NO_OUTPUTS | warning | node.outputs is empty |
FE_BODY_DUP_ID | error | Duplicate id within body_nodes |
FE_NO_BODY_ENTRY / FE_NO_BODY_EXIT | error | Missing body_entry / body_exit |
FE_BODY_ENTRY_MISSING / FE_BODY_EXIT_MISSING | error | body_entry/body_exit not in body_nodes |
FE_BODY_EDGE_DANGLING | error | body_edges references a nonexistent node |
FE_BODY_DUP_IO | error | Duplicate names in body_inputs / body_outputs |
FE_INLINE_OVERRIDES_TEMPLATE | warning | Both an inline body and body_template are set |
FE_BODY_UNDECLARED_VAR | error | A body node references an undeclared variable |
FE_BODY_INPUT_MISSING_IN_PARENT | warning | A body_inputs name has no corresponding variable in the parent graph |
FE_MERGE_DICT_NEEDS_RESULT_VAR | warning | merge_dict has no result_var set |
MISSING_OUTPUT_KEYS | error | The compute return dict is missing declared output keys |
NO_RETURN | warning | compute has no return |
NON_LITERAL_RETURN | warning | The compute return is not a literal dict |
SNIPPET_UNDEFINED_INPUT | error | The snippet references a variable not in node.inputs |
NO_TOOL_NAME | error | A native tool_call has no tool_name |
TOOL_NOT_CALLABLE | error | Tool is not in the catalog |
TOOL_NOT_IN_WHITELIST | error | Tool is not in the manifest tools.whitelist |
ARG_VAR_NOT_INPUT | error | An argument_map value is not in node.inputs |
BAD_RAG_SOURCE | error | Invalid rag_query source |
NO_QUERY_INPUT | error | rag_query has no query_input |
KB_NOT_IN_MANIFEST | error | kb_names not in manifest external_kbs |
NO_EVENT_TYPE | error | emit_event has no event_type |
UNKNOWN_EVENT_TYPE | error | event_type is not in manifest event_types |
NO_RESPONSE_TARGET | error | expects_response but no outputs / response_var |
OUTPUT_VAR_NOT_DECLARED | error | An output_map value is not in node.outputs |
INPUT_VAR_UNRESOLVED | error | A variable in node.inputs is neither injected by the host nor written by any node |
START_INPUT_UNUSED | error | query is not referenced by any downstream node |
OUTPUTS_NOT_BINDABLE | error | A native llm_call has outputs but no output_format/output_map/output_var |
BAD_SYSTEM_PROMPT_FILE | error | system_prompt_file is not ''/AGENTS.md/IDENTITY.md |
BAD_OUTPUT_FORMAT | error | Invalid output_format.type |
BAD_CLIENT_ID | error | client_id is not a string / not in the manifest |
TOOL_NOT_IN_MANIFEST | error | tool_whitelist is not in the manifest |
BAD_HISTORY_MODE | error | Invalid history_mode |
BAD_MAX_TURNS | error | max_turns is not a positive integer |
6. Sandbox & AST Allowlist
6.1 Permanently Denied
ctypes / pickle / importlib / inspect / code / codeop / pdb / trace / tracemalloc / sys / signal / multiprocessing / threading / asyncio / socket / select / asyncore / asynchat / ssl / mmap / platform / __import__ / eval / exec / compile / open / globals / locals / vars / getattr / setattr / delattr / breakpoint / input / exit / quit
6.2 Requestable Modules
Declared under capabilities.extra_modules, then gated by approval: os / subprocess / requests / aiohttp / httpx / sqlite3 / pathlib / tempfile / hashlib / hmac / base64 / binascii / uuid / secrets / shutil / glob / fnmatch / textwrap / io / copy / pprint / urllib / xml / csv / string / bisect / heapq / operator / itertools / random / fractions / warnings / contextlib / abc / types / argparse
Granting src.x unlocks the entire src/ subtree — use with care.
6.3 Capability Approval
Each (recipe_id, agent_id) pair has an independent approval hash. install_built_ins() auto-approves built-in recipes. The bind / setDefault responses carry caps_declared + needs_approval, and the frontend shows a CapabilityApprovalPanel.
7. Runtime Behavior
7.1 Node Execution
- compute: wraps
codeintodef _node_<uuid>(**inputs): ...and runs it inside the sandbox; the returned dict is written back to the variable pool. - llm_call custom:
args_builderbuilds the messages, the host calls ConversationSession, andresult_parserparses the observation. - llm_call native: the host calls ConversationSession directly from
config(with a tool_whitelist it runs a tool loop). - tool_call:
args_builderbuilds{tool_name, arguments}, and the host calls ToolDispatcher. - rag_query: the host runs RAG recall and
result_parserparses it. - emit_event: the host yields an event to the frontend; if the event_type has
expects_response=True, it blocks waiting for the user's response, which is written back toresponse_varor node.outputs. - for_each: expanded at runtime; each iteration creates a new variable scope with a snapshot of the parent pool plus the
iter_varbinding.
7.2 Variable Pool Semantics
- Global pool: shared by all nodes. Parallel branches writing the same variable → last-write-wins + warning.
- External variables: injected by the host at the start of a turn (query / history / attachments / agent_id / dialogue_id).
- for_each iteration scope: automatically includes a snapshot of the parent pool +
iter_var+<iter_var>_idx. Inline bodies are a strict allow-list: a body node can only read whatbody_inputsdeclares,iter_var, and what other body nodes write.
7.3 Recipe-SKILL Collaboration
A Recipe invokes SKILL.md through the execute_skill pseudo-tool; the SKILL takes effect when the recipe's knowledge/AGENTS.md exists. Agents inside a Flow see these tools via filtered_schemas, not tools.json.
8. Custom Renderers
8.1 Output Renderers (RecipeOutputRenderer)
{
"output_renderers": [
{"name": "couplet", "output_var": "final_output", "source_file": "couplet.vue"}
]
}.vue files live under renderers/; on save the backend's renderer_compiler pre-compiles them to ESM JS, which the frontend mounts when rendering chat output. The component receives :value="output_fields[output_var]".
8.2 Custom Event Types (RecipeEventType)
{
"event_types": [
{
"name": "approval_card",
"renderer": "approval_card.vue",
"fields": [
{"name": "title", "type": "string"},
{"name": "options", "type": "array", "items": {"name": "opt", "type": "string"}},
{"name": "audio_url", "type": "audio"}
],
"expects_response": true,
"response_var": "user_choice"
}
]
}The emit_event node uses event_type: "approval_card" + field_values: {...}. The runner resolves =expr, mounts the .vue, and waits for the response to be written back to response_var.
Supported field types: string / int / float / bool / array / object / audio / video / attachment / attachments. Media types produce a {kind, path} attachment dict that the frontend plays via the app-file:// protocol.
9. Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| tool_loop node idling | The LLM never emits a tool_call | In the system_prompt explicitly say "emit ≥1 tool_call/turn"; use a markdown output_format instead of a schema |
output_var not set | Schema fields spread to the top level and downstream .field access fails | Add output_var in config to wrap them |
.get() on a conditional edge | Silently evaluates to False | Use x['key'] subscript syntax |
Native llm_call has outputs but no output_format | Downstream done gets an empty string | Add output_format.type: "schema" or output_var |
| Changes to the translator don't take effect | Stale graph behavior | Restart the bridge or re-run install_built_ins |
copilot uses user_input instead of query | query is never consumed | START_INPUT_UNUSED will error out; always use query |
merge_dict without result_var | Later iterations overwrite earlier ones | Set result_var to the output variable of each iteration |
save_image_to_recipe times out | HTTP 600s is not enough | Set timeout_s: 1800 on the custom tool |
10. Development Workflow (End-to-End)
- Create a recipe in the Creative Workshop → drag nodes and connect edges in the editor;
- Edit node parameters (LLM prompt / tool / expression / renderer);
- Compile:
POST /api/recipe_dev/compile(structural validation;validate_graphoutputs violations); - Dry run:
POST /api/recipe_dev/dryrun(no real-model dependencies; uses a stub asker); - e2e test:
POST /api/recipe_dev/e2e_test(real LLM + tools); - Publish:
POST /api/recipe_dev/publish_relay(enters the review queue).
Reference Source Code
- Schema:
src/recipes/graph/schema.py - Validation:
src/recipes/graph/validate.py - Runtime:
src/recipes/graph/runner.py - Manifest:
src/recipes/manifest.py - Tool catalog:
src/recipes/catalog.py - REST:
src/bridge/routes/agent_recipes.py,src/bridge/routes/recipe_dev.py
