执行记录
执行记录是你按下"运行"后产生的可审计记录:状态、阶段日志、人工门控、最终产物。每次运行都可重放、可恢复、可分享。
执行的生命周期
每次运行都会走入五种终止状态之一。状态机如下:
running → failed
running → cancelled
| 状态 | 含义 | 你可以做什么 |
|---|---|---|
pending | 运行已排队但尚未真正开始。通常小于 1 秒。 | 等待。必要时取消。 |
running | 至少一个阶段正在进行中。阶段日志正在流式输出。 | 实时观看。取消。响应人工门控。 |
completed | 所有阶段都成功完成。所有产物都已保存。 | 下载产物。分享。用新输入重新运行。从后面某个阶段恢复以尝试改动。 |
failed | 某个阶段遇到错误或超时。后续阶段没有运行。 | 阅读错误。点击 "Debug in chat"。重试。或修复应用后重新运行。 |
cancelled | 你(或某人)显式终止了运行。 | 从头重新运行,或在已有部分完成阶段时从后面阶段恢复。 |
一条执行记录的结构
平台会把每一次执行持久化到 app_execution,包含以下字段:
"id": "exec_8f4c2e1b",
"app_id": "app_...",
"user_id": "usr_...",
"status": "completed",
"input": { company_name: "Stripe", ... },
"is_test": false,
"shared_session": false,
"start_from_stage_index": 0,
"prior_execution_id": null,
"duration_ms": 43210,
"error": null,
"created_at": "2026-05-31T09:14:22Z"
}
每个阶段也会拥有自己的 app_stage_log 条目:
"execution_id": "exec_8f4c2e1b",
"stage_index": 0,
"stage_type": "agent",
"status": "completed",
"goal_expanded": "Research Stripe and write a brief...",
"session_id": "sess_...", // 如果你点击 "Debug in chat",这里会关联到对应的会话
"duration_ms": 38421,
"error": null
}
goal_expanded 字段是输入替换之后的目标,也就是 agent 实际看到的内容。 如果你的表单里 company_name: "Stripe",那么原始目标中的 {{company_name}} 就会在这里被解析出来。
实时观看运行:SSE 流
在 running 状态下,应用页面会订阅一个以执行记录 ID 为键的 Server-Sent Events 流。 同一个流也是公开的 API 接口,任何想把运行情况镜像到其他系统的人都可以使用。端点如下:
Authorization: Bearer <token>
// 响应头
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
五种执行级事件
每个事件以 event: <name>\n data: <JSON>\n\n 的形式封帧。连接时,服务端 会先回放当前状态,让晚到的订阅者也能看到一个完整序列,当前执行状态、迄今为止的 每一条阶段日志、任何待处理的人工门控、每一个已保存的产物,然后再切换到实时广播。
event | 载荷结构 | 触发时机 |
|---|---|---|
status | { status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' } | 订阅时(回放)以及每次执行级状态变化时。 |
stage_log | 一整行 AppStageLog 数据,参见上文的结构。 | 订阅时(每条已有阶段日志一次)以及每次阶段状态变化时。 |
human_gate | 一整行 AppHumanGate 数据。 | 当 human 阶段开启一个门控时,以及门控被解决时再次触发。 |
artifact | 一整行 AppArtifact 数据,若存在则包含 content。 | 每次阶段输出被持久化时。 |
done | { status, duration_ms } | 在结束时触发一次,然后服务端关闭流。 |
如果你订阅一个已经处于终止状态的执行,服务端会回放已存储的阶段日志和产物,发送一个 done 事件,然后断开,不会建立实时连接。这样一来,回放和实时监控用的是同一个 端点和同一个解析器。
更细粒度的 runner 事件
上面五个执行级事件是汇总流。在一个 agent 阶段内部,底层的 run-agent 二进制会输出一份粒度细得多的 JSONL 轨迹,逐 token 文本、工具调用、 子任务、思考过程。这些事件位于会话级流而非执行级流,但它们正是右侧推理轨迹面板背后的 数据来源。完整事件目录请参见runner 协议。
最小 SSE 消费者
const es = new EventSource('/api/app-executions/' + execId + '/stream', {
withCredentials: true,
});
es.addEventListener('status', (e) => console.log('status', JSON.parse(e.data)));
es.addEventListener('stage_log', (e) => console.log('stage', JSON.parse(e.data)));
es.addEventListener('artifact', (e) => console.log('artifact', JSON.parse(e.data)));
es.addEventListener('human_gate', (e) => console.log('gate', JSON.parse(e.data)));
es.addEventListener('done', (e) => { console.log('done', JSON.parse(e.data)); es.close(); });测试运行与正式运行
在运行上设置 is_test: true,它就会被标记为临时性的。测试运行:
- 不会以正式运行同样的方式计入你的用量配额。
- 默认会从应用的"运行"标签中被过滤掉(切换"显示测试运行"可看到它们)。
- 使用独立的
shared_session 命名空间,这样队友的测试运行不会污染你的。 - 除非被钉住,30 天后自动清理。
当你点击试一下时,应用编辑器使用的就是测试运行,这正是你在迭代提示词 时想要的。
人工门控:暂停以等待审批
human 阶段不运行代码。它会暂停执行,展示目标文本和此前的产物,等待你(或另一位 有权限的用户)操作。这次暂停会存储在 app_human_gate 中:
{
"id": "gate_...",
"execution_id": "exec_...",
"stage_index": 2,
"message": "Review these 47 enriched leads. Remove rows you don't want to email.",
"status": "pending", // pending | approved | rejected
"response": null,
"responded_at": null
}门控在 UI 中长什么样
┌─────────────────────────────── Run #exec_8f4c2 ─────────────────┐
│ Stage 3 of 4 · Review paused │
│ │
│ Review these 47 enriched leads. Remove rows you don't want │
│ to email. │
│ │
│ 📎 enriched_leads.csv (47 rows) [ View ] │
│ │
│ [ Approve & continue ] [ Reject ] [ Edit input first ] │
└──────────────────────────────────────────────────────────────────┘三个按钮
- Approve & continue(批准并继续),门控变为
approved,下一个阶段立即开始。 - Reject(拒绝),门控变为
rejected,运行转入 failed。 - Edit input first(先编辑输入),打开产物以编辑(例如从 CSV 中删除行),保存后,编辑后的版本就是第 n+1 阶段读取的内容。
找到需要你处理的门控
从顶部导航打开待处理的门控,或调用 GET /api/app-executions/pending-gates。门控在变为 pending 时可以 配置通过邮件或 Slack ping 你,这对你可能不会盯着看的长任务流水线很有用。
门控 API
通过向门控的解决端点 POST 来批准或拒绝它。请求体携带判定结果以及可选的自由文本 response,存储在门控行上:
POST /api/app-executions/:execId/gates/:gateId
Content-Type: application/json
Authorization: Bearer <token>
{
"action": "approved" // 或 "rejected",
"response": "Removed 4 lookalikes; lists looks tighter now."
}机制上:服务端会标记 app_human_gate.status 并写入 responded_at, 然后解决一个执行器一直在 await 的内存 Promise。等待中的阶段立刻从 waiting 状态切出,流水线要么进入第 N+1 阶段(批准时),要么短路到 failed(拒绝时)。一个携带更新后行的 human_gate SSE 事件会扇出 给所有订阅者。
在门控待处理期间,执行始终保持 running。 只有阶段日志会进入 waiting。沙箱会通过 keepAlive 保活,这样在 批准后恢复时不需要付冷启动代价。代价是:一个无限期待处理的门控会一直占着一个热沙箱,请给审核者一个合理的预期,或者在应用设计中给门控加上自动超时。取消运行
在正在运行的执行上点击取消,或发送:
POST /api/app-executions/:execId/cancel
Authorization: Bearer <your_token>取消会:
- 停止当前正在运行的阶段。已完成的阶段保持完成状态,它们的产物会被保留。
- 关闭沙箱。
- 将执行转入
cancelled。 - 把未消耗的预算退回到你的余额。
之后你可以从下一个阶段恢复:见下文。
从指定阶段恢复
一个真正强大的能力:只重新运行需要重跑的阶段,复用那些已经跑通的阶段产物。在运行请求里 传入 start_from_stage_index 和 prior_execution_id:
POST /api/apps/:appId/run
Content-Type: application/json
{
"input": { ... same shape as the original run ... },
"start_from_stage_index": 2,
"prior_execution_id": "exec_8f4c2e1b"
}执行器实际做了什么
- 创建一行新的
app_execution,指向同一个应用版本。对于索引 0 .. start_from_stage_index - 1 的阶段日志,会立即以 status: 'skipped' 写入,运行历史会清楚地标明哪些阶段没有重新执行。 - 通过
dao.listArtifactsByStages(priorExecutionId, [stageIds]) 加载被跳过 阶段的产物。对于不携带内联 content 的产物,执行器会按 s3_key 从 S3 下载主体并按 UTF-8 解码,这样即使原始行已经卸载,下一阶段 的系统提示词也能看到之前的内容。 - 申请一个全新的沙箱。原执行沙箱上第 0-1 阶段写入的文件没有了,只有已持久化的 产物会穿越边界。如果后续阶段依赖的是中间临时文件而不是声明的产物,那么恢复就重现不 了它们。
- 构建第
start_from_stage_index 阶段的系统提示词,把加载到的之前产物作为 ## Previous Stage Outputs 注入,然后从那里一直跑到流水线末尾。
恢复 + shared_session
当原始运行使用了 shared_session: true,并且恢复运行也请求了它时,执行器会 更进一步:它会在 prior_execution_id 上定位一条已经有 session_id 的阶段日志,复用那个会话,继续同一段 Claude 对话。可见效果:Debug in chat 会显示一个跨越原始运行和恢复运行的连续线程。不要无脑启用,共享 Claude 会话意味着模型 的上下文仍包含此前的轮次,有时这正是你要的,有时不是。
什么时候会用到它
- 最后一个阶段的草稿不对。第 0-2 阶段花了 4 分钟跑得好好的;第 3 阶段的 邮件草稿语气不对。调一下应用,然后从第 3 阶段重跑,省下 4 分钟。
- 阶段因为外部原因失败(Connect 令牌运行中过期)。修好 Connect,用同样的 输入从失败阶段重跑。
- 人工拒绝了一个门控。调整前一阶段的提示词,从那里重跑。
恢复不会回滚已经产生的副作用。如果被恢复的阶段已经往你的 CRM 写入了行、 发出了 Slack 消息、或推送了一次 git 提交,这些工作已经实实在在发生了。平台不跟踪对外 副作用;恢复只复用已持久化的产物。为了幂等性,请让会触及 Connect 的阶段先检查 "我是不是已经做过这件事了?"。在 UI 里:打开运行,点击从阶段重新运行……,选择阶段。
运行历史
在应用页面,运行标签会按时间倒序列出每一次执行。每一行展示:
列 展示内容 状态 五个状态徽章之一(pending / running / completed / failed / cancelled)。 触发者 你、队友、定时任务,或 API。 开始时间 以你工作区时区显示的时间戳。 耗时 所有阶段的总墙钟时间。 成本 沙箱秒数 + LLM token 换算成美元。 输入 所用的表单值。点击展开。 产物 数量,并附带快速下载入口。
顶部的筛选条件让你可以按状态、日期范围、触发者和标签来缩小范围。该列表也可通过 GET /api/app-executions?app_id=... 获取。
调试失败的运行
第 1 步:阅读错误信息
打开失败的运行。页面顶部第一项就是错误信息以及出错的阶段。常见形式:
错误 几乎总意味着 stage_timeout阶段耗时超过了 timeout_ms。调大它。 connect_unauthorized某个 Connect 被吊销或过期了。重新授权。 required_input_missing有必填的表单字段是空的。不要把字段标得过于激进地"必填"。 artifact_format_mismatch阶段产出的格式不对(例如声明的是 CSV,结果输出了 Markdown)。 sandbox_oom沙箱内存耗尽。数据量太大,拆分阶段,或使用脚本阶段。 agent_gave_upagent 判断任务做不下去了。阅读它的推理,通常是少了某个输入。
第 2 步:点击 "Debug in chat"
点击 Debug in chat。会打开一个携带失败运行完整上下文的对话,输入、部分产物、 agent 推理轨迹、错误。直接对 agent 说话:
> Why did this fail? What should I change in the App?agent 会诊断问题、建议修复方案,并且(通常会)直接编辑应用来应用修复。然后你可以从同一个 位置重跑。
第 3 步:重试或重新运行
- 重试:相同输入、相同应用版本、全新沙箱。有时候不稳定的外部 API 只是 需要再试一次。
- 从第 N 阶段重跑:保留之前的产物。当只有后段阶段坏了时使用。
- 编辑应用后再跑:用于系统性 bug(提示词模糊、格式不对)。会让应用升一个新版本。
分享一次执行记录
你可以把一次执行的只读视图给队友看,适合"看看这个应用昨天产出了什么"这种场景,又不必 给对方应用本身的写权限。
- 打开运行。
- 点击分享。
- 配置:包含哪些产物、有效期、是否允许下载、是否设密码门控。
- 获得一个公开链接:
https://app.aitroop.net/s/<token>。
分享记录会持久化到 share 表中,带有 token、解锁策略和下载处理器。可以随时从 设置 → 分享撤销。
通过 API 触发运行
当你想从一个 webhook、CI 流水线或另一个 agent 触发一个应用时很有用。最小调用:
curl -X POST https://app.aitroop.net/api/apps/<appId>/run \
-H "Authorization: Bearer $AT_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "input": { "company_name": "Stripe" } }'响应是新的 app_execution,状态为 status: "pending"。要观察它:
curl https://app.aitroop.net/api/app-executions/<execId>/stream \
-H "Authorization: Bearer $AT_USER_TOKEN" \
-NSSE 流会一直输出直到运行进入终止状态。或者轮询 GET /api/app-executions/:execId。
常见问题 & 故障排查
我的运行卡在 pending 几分钟了。怎么回事?
可能原因:
- 沙箱服务商冷启动(罕见;通常小于 1 秒)。
- 你触到了套餐的并发运行配额;运行在排其他运行的队。
- agent worker 队列堵了(设置 → 工作区顶部会有状态横幅)。
解决:如果超过 2 分钟,取消并重跑。检查状态横幅。如果一切都正常但仍然卡着, 说明运行在排队,等一下或取消。
为什么我已完成的运行显示"0 个产物"?
应用的阶段没有声明任何 artifact_defs,或者 agent 产出的格式不符合声明,平台把它 丢掉了。打开运行,滚动到对应阶段,找一找"format mismatch"警告。把目标改得更明确地说明要产出 什么,或者把声明的格式改成 file 作为兜底。
我取消了一个运行,能"反取消"吗?
不行,但你可以从最后一个完成的阶段重新运行。前一次执行的产物已保留,所以 你不会丢失工作。打开被取消的运行,点击从阶段重新运行……,选择最后一个已 完成阶段之后的那个阶段。
一个人工门控已经待处理 3 天了。运行死了吗?
没有。门控默认无限期等待。执行只是停在那里,没有失败。要清理它,要么响应这个门控,要么取消 运行。
如果你希望门控自动过期,在阶段定义里加一个超时: "gate_timeout_ms": 86400000 表示 24 小时。超时后,门控转入 rejected,运行转入 failed。
我能看到 agent 当时在想什么吗?
可以。在运行的时间线上点击任意阶段,展开它的推理轨迹。轨迹会显示 agent 的计划、每一次工具 调用(包括参数和结果),以及它最后的评注。要看更深的细节,开启"显示思考过程",这会 暴露模型的内部推理块(当可用时)。
为什么同一个应用这周比上周慢了一倍?
可能原因:agent 调用了更多次工具。可能是提示词改了(看一下版本), 处理的数据变多了,或者某个外部 API 变慢了。把两次运行并排打开:运行日志会显示每次工具调用 的次数和耗时。对比一下就能找到回归点。
我想 A/B 测试同一个应用的两个版本。怎么做?
Fork 这个应用(会创建一份私有副本 v1),编辑副本,然后用相同的输入运行两个应用。从各自应用 的"运行"标签里并排比较两次运行。要做统计层面的深入分析,写一个定时任务,每天用相同的输入 运行两个版本,并把结果导入到一张表里。
我能从 Zapier / n8n / CI 里的 curl 命令触发运行吗?
可以,用你的 bearer token 调用 POST /api/apps/:appId/run。参考上面的 curl。多数 用户会把这个调用放在一个 webhook 处理器里,再轮询 GET /api/app-executions/:execId 来检测完成。完成后的对外 webhook 也可通过定时任务的投递目标来支持。