核心概念
Aitroop 应用的技术结构参考:每一个字段、每一种类型、每一个选项。可收藏作为查阅手册。
应用定义
一个应用是持久化在 app_def 中的 JSON 文档。你不需要自己编写 JSON(这由 agent 完成),但了解它包含什么会让其他一切变得清晰。顶层结构如下:
"id": string, // 自动生成,"app_..."
"name": string, // "公司简报"
"description": string, // 一句话摘要
"icon": string, // emoji,例如 "🏢"
"tags": string[], // ["research", "b2b"]
"status": "draft" | "published" | "archived",
"version": number, // 单调递增;每次成功保存时递增
"input_schema": AppInput[], // 表单
"stages": AppStage[], // 要做什么,按顺序
"artifact_schema": AppArtifactDef[], // 要产出什么
"resources": { skills: string[], connects: string[] },
"examples": AppExample[] // 可选的预填示例
}
简写形式的公式:
AppInput:表单字段
input_schema 中的每一项都是用户填写的一个表单字段。完整类型如下:
"id": string, // snake_case,例如 "company_name"
"label": string, // 显示在字段上方
"type": AppInputType, // 见下方表格
"required": boolean, // 默认 false;为空时阻止运行
"placeholder": string, // 可选提示,显示在字段内
"default": unknown, // 可选的预填值
"dynamic_default":string, // "{{today}}" 等:在运行时解析
"description": string, // 显示在下方的帮助文字
"options": { label, value }[], // select/multiselect/radio 必填
"show_if": AppInputConditionalRule, // 条件可见性
"min" | "max" | "step": number // 用于 number/slider
}
11 种输入类型
服务端会根据固定枚举校验 type;提交未知类型会返回 400。完整列表:
| 类型 | UI | 使用场景 | 额外字段 |
|---|---|---|---|
text | 单行文本框 | 短标识:名称、URL、ID。 | - |
textarea | 多行文本框 | 长指令、描述、待处理的内容。 | - |
number | 数字输入 | 计数、阈值、价格。 | min、max、step |
slider | 数字滑块 | 有界范围,注重感觉而非精度。 | min、max、step |
select | 单选下拉 | 3-10 个固定选项,仅允许选一项。 | options(必填) |
multiselect | 多选下拉 | 3-10 个固定选项,允许多选。 | options(必填) |
radio | 单选按钮组 | 2-4 个选项,需要全部一次性展示。 | options(必填) |
boolean | 开关 / 复选框 | 是 / 否标志、可选特性。 | - |
date | 日期选择器 | "截止日期"、"锚定日期"。 | - |
daterange | 两个日期选择器 | 区间报表,"从 X 到 Y"。 | - |
file | 文件上传按钮 | 用户每次运行都上传新文件(CSV、PDF、图片)。 | - |
动态默认值
dynamic_default 是一个固定枚举,不是模板语言。选用其中之一会在运行时填入字段,而不需要静态默认值。适合定时任务中需要可滑动锚点的场景。
| 取值 | 解析为 |
|---|---|
today | 工作区时区下的今天。 |
yesterday | 今天的前一天。 |
last7days | 以今天为终点的 7 天窗口(搭配 daterange 使用)。 |
last30days | 以今天为终点的 30 天窗口。 |
thisMonth | 本月 1 日至今天。 |
lastMonth | 上月 1 日至上月最后一天。 |
示例:一个 3 字段的表单
{ "id": "company_name", "label": "Company name",
"type": "text", "required": true,
"placeholder": "Acme Corp" },
{ "id": "tone", "label": "Tone",
"type": "select", "default": "executive",
"options": [
{ "label": "Executive", "value": "executive" },
{ "label": "Engineering", "value": "eng" },
{ "label": "Sales", "value": "sales" }
] },
{ "id": "include_competitors", "label": "Include competitors?",
"type": "boolean", "default": true }
]
required。对于与时间相关的输入,使用 dynamic_default({{today}}、{{last_week_start}}),这样定时运行根本不需要任何输入。AppStage:实际执行的内容
stages 中的每一项都是工作流中的一步。各阶段按顺序运行;每个阶段都可以读取之前阶段产生的产物。
"id": string, // snake_case,例如 "research"
"name": string, // 显示名称
"stage_type": "agent" | "script" | "human", // 默认 "agent"
"goal": string, // 含 {{refs}} 的指令
"system_prompt": string, // 可选的附加上下文
"model": string, // 默认 "claude-sonnet-4-6"
"timeout_ms": number, // 默认 180000(3 分钟)
"skills": string[], // 阶段级技能列表(覆盖应用级)
"connects": string[], // 阶段级连接列表
"artifact_defs": AppArtifactDef[],// 此阶段产生的产物
// 当 stage_type == "script" 时:
"script_lang": "python" | "node",
"script_code": string,
// 当 stage_type == "human" 时:
"human_message": string // 展示给审批者的内容
}
对比三种阶段类型
agent 默认 | script | human | |
|---|---|---|---|
| 运行什么 | Claude,具备完整推理与工具调用 | Python 或 Node 代码,无 LLM | 等待真人 |
| 适用场景 | 研究、起草、摘要,任何模糊任务 | 精确转换:过滤 CSV、去重、格式化日期 | 审批关卡、"挑出最好的 N 个" |
| 延迟 | 20 秒 - 10 分钟 | 通常 < 1 秒 | 取决于真人耗时 |
| 成本来源 | LLM token + 沙箱时长 | 仅沙箱时长 | 零计算成本 |
| 确定性? | 否(LLM 的随机性) | 是:同样输入,同样输出 | 否(人的可变性) |
编写 goal 字段
goal 就是 agent 的指令。在阶段运行之前,执行器会用正则 /{{([^}]+)}}/g 遍历 goal,并将每个 {{key}} 替换为 execution.input[key]。未匹配的引用会原样保留:并不会报错,但也不会被替换,所以请仔细拼写输入的 id。
| 引用 | 解析为 |
|---|---|
{{company_name}} | 用户在 company_name 输入框填入的值。 |
{{include_competitors}} | 布尔值的字符串形式("true" / "false")。 |
{{date_range}} | 一个 daterange 输入在字符串化时会呈现为其 JSON 形式。 |
goal 模板不支持的内容。这里没有 {{today}}、没有 {{user.email}}、没有 {{#if x}}...{{/if}},并且关键是没有 {{ stages.X.Y }}。模板展开器只看表单值。对于时间相关的锚点,请改用输入字段上的 dynamic_default。对于上一阶段的输出,请见下面的多阶段应用:执行器会自动把它们注入到 system prompt。一个真实示例:
Research {{company_name}} ({{company_url}}) and write a comprehensive
competitive analysis covering:
1. Business model and revenue streams
2. Key products/services
3. Target customers and ICP
4. Recent news and strategic moves
5. Strengths, weaknesses, opportunities, threats
If {{include_competitors}} is true, also include
the top 3-5 direct competitors with a one-paragraph profile each.
Produce a Markdown report titled "{{company_name}} Competitive
Analysis" under a ## Competitive Analysis heading.一个好的 goal 应该是:
- 具体:指明要用哪些输入以及如何使用它们。
- 关注输出:指明产物 ID 与格式。
- 可执行:使用祈使动词("研究"、"撰写"、"分析")。
- 结构化:一个带编号小节的 10 行 goal 几乎总是优于 2 行的版本。
AppArtifactDef:每个阶段的产出
{
"id": string, // snake_case,例如 "final_report"
"title": string, // 显示名称
"format": ArtifactFormat, // 见下方表格
"language": string, // 用于 format="code",例如 "python"
"description": string
}支持七种格式:
format适用 预览 markdown报告、简报、草稿 按 GFM 渲染,支持表格、代码围栏、Mermaid。 code源代码,单文件或目录树 语法高亮,多文件输出时附带文件树。 html渲染页面、仪表盘、幻灯片 沙箱化的 iframe;允许脚本但被隔离。 json用于下游系统的结构化数据 可折叠树,附类型信息。 csv表格数据:线索、交易 可排序、可筛选的电子表格视图。 image生成的图片、图表、示意图 支持缩放与平移。PNG / JPEG / SVG / WebP。 file其他任何内容:PDF、ZIP、音频、视频 按类型自适应:PDF 内嵌预览、音频播放器、ZIP 文件列表。
所有产物都存储在 S3(如果你是自托管则存在 R2)中,位置为 app_artifact.s3_key,并记录 MIME 类型与字节大小。它们会在应用的整个生命周期中保留;删除的应用归档在 30 天内仍可恢复。
Resources:应用所需的技能与连接
"resources": {
"skills": ["web-search", "pdf-extract"],
"connects": [
{ "type": "specific", "id": "hubspot", "is_required": true },
{ "type": "category", "id": "email", "is_required": true,
"label": "Any inbox we can send from" }
]
}技能是 agent 可以调用的能力模块(网页搜索、PDF 提取、图像生成),详见 技能。连接是应用所需的 OAuth 桥梁。Aitroop 用一个两级分类来组织连接:
type: "specific":按 connect_definition.id 锁定单一提供方(例如 github、hubspot、gmail)。仅适配某一种集成时使用。type: "category":接受 connect_category 内的任一提供方(例如 email、storage、crm)。只要用户授权了该类别下的至少一个提供方,应用就视为满足。当应用与具体提供方无关时("任意邮箱"、"任意 CRM")使用。
旧的形式 "connects": ["hubspot", "github"] 仍然可用:应用运行时每个字符串都会被当作 { type: "specific", id: «string», is_required: true }。应用构建器在新建应用时会写入更丰富的形式。
资源可以在应用级别声明(如上),也可以按阶段声明(在 stage.skills / stage.connects 下)。按阶段范围化是让多阶段应用更安全的方式:给阶段 1 只配置 web-search,给阶段 2 只配置 CRM 连接,以此类推。请见 连接 了解完整的提供方目录与满足模型。
执行记录:应用真正运行的时刻
一次执行记录是一次走完应用所有阶段的过程。它就是那个带有状态、阶段日志、耗时和最终产物集合的实体。
{
"id": "exec_...",
"app_id": "app_...",
"status": "pending" | "running" | "completed" | "failed" | "cancelled",
"input": { ...form values },
"is_test": boolean,
"duration_ms": number,
"start_from_stage_index": number, // 用于恢复
"prior_execution_id": string | null // 恢复时使用
}执行中的每个阶段都会有自己的 app_stage_log 行,包含状态、耗时、session ID(可据此打开为对话)、展开后的 goal 以及任何错误。
两个层级的状态枚举并不一致:
字段 可能取值 app_execution.statuspending · running · completed · failed · cancelledapp_stage_log.statuspending · running · waiting · completed · failed · skippedapp_human_gate.statuspending · approved · rejected
当一个 human 阶段在等待时,阶段日志会进入 waiting;父执行仍保持 running。当执行带 start_from_stage_index > 0 恢复时,它们也会进入 skipped:较早的阶段不会重新运行。请见 执行记录 了解完整生命周期、调试流程与 SSE 流式推送。
沙箱:agent 实际工作的地方
每一次对话和每一个应用阶段都运行在一个沙箱中:一个隔离的运行时,拥有自己的文件系统、自己的进程和出站网络。agent 可以 pip install、运行 shell 命令、写文件、抓取 URL,运行结束时沙箱会被销毁。
统一在 ISandbox 接口下提供三种后端,再由一个路由器在它们之间选择:
host:直接在 Aitroop 服务进程中运行。仅用于自托管开发环境;DATABASE_URL、JWT_SECRET、E2B_API_KEY 与 S3 凭据等敏感变量会从子进程环境中剥离。e2b:来自 E2B 的 Firecracker 微型虚拟机。Aitroop 托管版的默认选项。daytona:来自 Daytona 的托管开发容器。适合重负载 / 长时阶段。router:按用户分发到上述三者之一的选择器(用于把特权用户路由到 host)。
无论使用哪个提供方,每个沙箱都暴露相同的目录布局,根目录是沙箱用户的 home 目录:
~/.aitroop/ ← Aitroop 工作目录
~/.aitroop/run-agent-config.json ← 每次运行的配置
~/.aitroop/run-agent ← 二进制(远程模式下上传)
~/.aitroop/skills/{name}/{version}/ ← 不可变的技能缓存(与 S3 镜像)
~/.claude/skills/{name} ← 软链 → 缓存(始终启用的核心技能)
~/.aitroop/app_skills/{appId}/.claude/skills/ ← --add-dir 目标(应用专属技能)
{projectDir} ← agent 读写文件的地方同一次执行中的所有阶段共享同一个沙箱,所以阶段 2 可以读取阶段 1 写入的内容。不同的执行使用不同的沙箱,不会跨运行泄漏。实践中你会遇到的常量:SANDBOX_IDLE_TIMEOUT_MS = 5 分钟、SANDBOX_KEEPALIVE_INTERVAL_MS = 30 秒,以及上限 AGENT_RUN_TIMEOUT_MS = 7 天。请见 沙箱 了解完整的运行模型。
多阶段应用
当一个工作流天然有彼此衔接的步骤时,就定义多个阶段。各阶段按顺序运行。每个阶段在两个地方都能看到前序阶段的产物,但都不通过 goal 模板:
- 共享的沙箱文件系统。一次执行中的所有阶段都在同一个沙箱中运行(沙箱)。阶段 1 写入的文件,阶段 2 仍然可以读到。
- system prompt 中自动注入的
## Previous Stage Outputs 段落。在阶段 N 运行之前,执行器会加载阶段 0..N-1 产生的所有产物,把每个产物渲染为 ### {{title}}\n{{content}},并将这一段拼接到 system prompt 之前。agent 把它们当作普通上下文阅读,不需要在 goal 中手动引用。
App: Cold Outreach Pipeline
stages: [
{ id: "find", stage_type: "agent",
goal: "Find companies matching {{icp}}...",
artifact_defs: [{ id: "leads", title: "Leads", format: "csv" }] },
{ id: "enrich", stage_type: "agent",
goal: "Read the Leads CSV from the previous stage and add
a verified email and recent news per row...",
artifact_defs: [{ id: "enriched", title: "Enriched Leads", format: "csv" }] },
{ id: "review", stage_type: "human",
human_message: "Review the enriched leads. Remove rows
you don't want to contact." },
{ id: "draft", stage_type: "agent",
goal: "Draft personalized outreach for the approved leads...",
artifact_defs: [{ id: "drafts", title: "Drafts", format: "markdown" }] }
]注意两个 agent 阶段之间夹着一个 human 阶段,这就是人在回路(human-in-the-loop)模式。执行在此处暂停,平台会在 app_human_gate 中创建一行,状态为 pending,阶段日志切换为 waiting。只有当 gate 被设为 approved 时下一个阶段才会运行(设为 rejected 时则保持暂停)。
会话与消息队列
所有与 agent 的对话(无论是自由聊天、应用阶段,还是定时任务的一次触发)都发生在一个会话中。一个会话是一个稳定的容器,拥有自己的 Claude 对话 ID、自己的有序消息日志,以及自己的状态追踪队列。在 app_session.session_type 中持久化有三种形态:
session_type 由谁创建 生命周期 出现位置 chat用户:"+ 新对话"。 除非归档/删除否则永久保留。 侧边栏、搜索、历史。 app_stage应用执行器在某个 agent 阶段开始时创建。 绑定到父执行。永久存储作为审计,可通过在对话中调试浏览。 执行详情页:每个阶段日志都带有 session_id。 scheduled某个 send_message 定时任务的第一次触发。 之后该任务的每次触发都复用:设计上就保持连续性。 从任务行链接而来的单个 "[Scheduled] {name}" 会话。
在每个会话中,每一次用户回合都是 app_message 中的一行。这些行不是"发完就忘",它们要走一个小型状态机,因为 agent worker 池可能是多实例的,同一个会话必须串行化它的分发:
pending → dispatching → sent
dispatching → failed (5 次以上尝试后)app_message 上重要的列:
position:每个会话内的整数,单调递增。决定相同优先级行之间的顺序。DAO 在插入行时原子地分配此值。priority:64 位排序键。优先级越高越先分发;默认为壁钟时间,所以两条没有显式优先级的消息按到达顺序运行。attempts:每次尝试分发时递增。5 次失败后,行进入 failed,用户可见消息显示错误。dispatched_at:worker 认领该行时设置;与 15 分钟过期阈值配合驱动恢复轮询。mode:自由形式的标签,表示这条消息是什么类型(用于 UI 中路由格式化)。last_error:最近一次失败原因,跨重试保留。
worker 通过 SELECT … FOR UPDATE SKIP LOCKED 并按 priority DESC, position ASC 排序来认领工作,因此两个实例永远不会抢到同一行。一个部分唯一索引(在 session_id 上的 uq_app_message_inflight,WHERE 条件为 status = 'dispatching' AND role = 'user')在数据库层强制"每个会话最多一条在途用户消息"。如果某行在 dispatching 状态停留超过 900 秒(worker 崩溃、节点丢失),恢复轮询器会将其重置为 pending,以便另一个 worker 接手。
分发是如何流转的
机制有两层:进程内的 QueueController 与 Postgres 的 NOTIFY 频道 queue_dispatch。当一条新消息入队时,写入侧实例会在本地广播 queue_updated 事件,并发出 pg_notify('queue_dispatch', '{sessionId}|{originInstanceId}')。监听同一频道的其他实例会忽略自己的回声,并接手工作。因此分发在集群范围内是事件驱动的,再辅以 60 秒的轮询作为漏掉通知时的兜底。
shared-session 标志
默认情况下,每个应用阶段都会得到自己的会话。当 RunAppRequest.shared_session 为 true 时,执行器会为整次执行创建一个会话,并在每个 agent 阶段复用其 session_id。可见差异:在执行上点击在对话中调试会打开一个包含每个阶段消息的单一对话,而不是每个阶段一个对话。在你希望按叙事的方式阅读整次运行时有用;其他场景很少需要。
ask_user 模式
agent 偶尔需要在运行中向用户提问(一个歧义、一段缺失的输入)。发出一个 ask_user 工具调用会暂停 agent 当前回合,将问题在对话 UI 中呈现,并等待回复。平台会在下一次 tick 时把回复作为工具结果回传,所以从模型视角看,agent 的"我问过了,这是答案"只是一次普通的对话步骤。这与 human 阶段(在执行级别对整个流水线设置关卡)不同;ask_user 完全是会话内部行为。
agent runner 协议
每个 agent 阶段和每次对话回合最终都通过一个名为 run-agent 的 Go 二进制运行。服务器把一个 JSON 配置交给该二进制,并按行读取它的 stdout。每一行就是一个事件:这是一个稳定的 JSONL 协议,你会在日志、SSE 负载以及对话 UI 的推理轨迹中看到它的引用。
转发的事件
这些事件会在到达时被推送到 UI。线上字段名使用 camelCase 以方便 JS;runner 的 snake_case 会自动转换(tool_id → toolId、is_error → isError 等)。
事件 含义 text_deltaagent 正在写出可见文本。按 token 流式输出。 thinking_delta隐藏的推理内容。仅在轨迹面板中显示。 tool_startagent 正在调用一个工具。携带 toolId、toolName 和参数。 tool_result工具返回。携带 toolId、输出与 isError。 subtask_start / subtask_progress / subtask_doneagent 派生了一个嵌套任务(Task 工具 / 子 agent)。 ask_useragent 在继续之前需要一个问题被回答(见上文)。 context_usage周期性的 token 用量遥测(输入/输出/缓存命中)。 session_info携带本次回合背后的 Claude 对话 ID;用于恢复。 system平台级消息(例如权限提示、重试)。 erroragent 端错误;本回合失败。 done回合干净地完成。
内部事件(永不转发)
三个以 __runner_*__ 命名的事件只被服务器消费:它们是簿记,不是用户可见的状态:
事件 用途 __runner_heartbeat__保活 ping。即使没有 token 在流,也告诉监管者子进程仍存活。 __runner_done__子进程干净退出。让监管者把正常结束和崩溃区分开。 __runner_error__子进程崩溃;携带捕获到的 stderr。触发 session-expired / sandbox-not-found 分类(见下文)。
错误分类
并非每一种 runner 失败都一样。服务器会把捕获到的错误信息归类到以下五个桶之一,桶决定了恢复策略:
类别 触发条件 恢复 session_expired背后的 Claude 对话被退役或轮换。 开启新的 Claude 对话;保留 Aitroop 会话行。 sandbox_not_found沙箱容器已死或被回收。 驱逐缓存的沙箱并新建一个;本回合重试一次。 network传输错误,如 ECONNRESET、ETIMEDOUT。 退避后重试。 connectiongRPC / MCP 协议级错误。 把错误暴露出来;由用户显式重试。 unknown其他任何情况。 以原始错误把回合标记为失败。
运行配置里有什么
服务器为每个回合构建一份 RunAgentConfig,并在调用二进制之前写入沙箱中的 ~/.aitroop/run-agent-config.json。要点字段:
prompt:实际的用户消息(或阶段展开后的 goal)。options.model:Claude 模型 ID;若阶段未锁定则默认为 claude-sonnet-4-6。options.resume:存在时表示恢复一个已有的 Claude 对话。服务器从会话的 agent_session_id 读取。options.allowedTools:runner 向模型暴露的精选工具集(通常 8 个:Read、Edit、Bash、Glob、Grep、Write、Task,以及 Aitroop MCP 集合)。options.permissionMode:固定为 'bypassPermissions';平台在沙箱边界强制权限,而不是交互式询问。options.mcpServers:Aitroop 自研的 MCP 服务器,加上应用 / 阶段请求的其他 MCP。options.env:process.env 中所有以 SANDBOX_ENV_ 开头的变量都会被转发并去掉前缀,再加上 AT_USER_TOKEN(用户的 JWT)被注入,以便沙箱内的服务端调用能通过认证。options.oauthToken / 代理模式:要么是 Anthropic OAuth token,要么在配置了代理时使用一个指向代理的 ANTHROPIC_BASE_URL,并以用户的 JWT 作为 bearer。平台在每次调用时快照一次,保证一次回合内的所有重试都使用相同凭据。
应用是如何被创建出来的
你不需要手写应用 JSON。这由 agent 完成。流程:
- 你在对话中告诉 agent:"创建一个应用,让它……"
- agent 激活
aitroop-app-create 技能(应用构建器)。 - 必要时应用构建器会问 1-5 个澄清问题。
- 它设计完整的 AppDef(输入、阶段、产物、资源)。
- 它校验设计:每个 ID 唯一、每个
{{ref}} 可解析、goal 足够具体。 - 它调用
POST /api/apps 来保存。 - 它报告应用 ID 并提议进一步打磨。
要编辑已有的应用,你可以说类似 "更新我的'公司简报'应用,加入招聘数据",应用构建器会拉取当前版本、修改它,然后调用 PUT /api/apps/{id}。
常见错误
- goal 过于模糊:"做点研究"过于宽泛。请指明信息源、深度、输出格式。
- 缺少产物:一个没有
artifact_defs 的应用不会产生任何可见输出。 - 必填项太多:用户会放弃需要长表单的应用。能默认就默认。
- 超时设得太短:重度研究阶段需要 5-10 分钟。把
timeout_ms 调到 600000。 - 输入类型选错:长文本用
textarea,短标识用 text,3-6 个固定选项用 select。 - 过度拆分:把本是一个想法的任务拆成 4 个阶段。只有当工作天然有可分离的步骤时才使用多阶段。
速查词表
术语 含义 应用(App) 一份保存好的工作流 JSON 文档,包含表单、阶段与产物。 AppInput 一个表单字段。支持 11 种类型,详见输入类型表。 阶段(Stage) 工作流中的一步。类型:agent、script、human。 goal 阶段内 agent 的指令字符串。用 {{id}} 引用输入。 产物(Artifact) 阶段产出的交付物。带类型(markdown、csv 等)。 技能(Skill) agent 可以调用的能力(web-search、pdf-extract 等)。 连接(Connect) 到外部服务的 OAuth 集成(google、github、hubspot)。 Resources 应用声明所需的技能与连接。 执行记录(Execution) 一次应用的运行。带有状态、阶段日志与产物。 阶段日志(Stage log) 一次执行中按阶段的记录:状态、耗时、错误、会话。 Human gate 等待人工通过 / 拒绝 / 编辑的暂停执行。 沙箱(Sandbox) 每次对话或应用运行所在的隔离运行时。三种提供方:host / e2b / daytona。 会话(Session) 承载消息序列的对话容器。类型:chat / app_stage / scheduled。 消息(Message) app_message 中的一行。处于分发队列中,具有状态机与优先级。Runner 实际驱动一次 Claude 回合的 run-agent 二进制。向服务器发回 JSONL 事件。 工作区(Workspace) 账户容器:包含应用、连接与运行历史。 Org 位于用户之上的团队层。共享应用、共享账单、成员角色。