文档 运行 执行记录

执行记录

执行记录是你按下"运行"后产生的可审计记录:状态、阶段日志、人工门控、最终产物。每次运行都可重放、可恢复、可分享。

心智模型。一个应用是菜谱,一次执行记录是你实际下厨的那一次。 菜谱在周二晚饭和周三晚饭之间不会变;变的是执行记录,表单里的食材不同、沙箱不同、输出不同、 时间戳不同。平台会永久保留每一次执行,方便你比较、调试和重放。

执行的生命周期

每次运行都会走入五种终止状态之一。状态机如下:

pending  →  running  →  completed

                     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 接口,任何想把运行情况镜像到其他系统的人都可以使用。端点如下:

GET /api/app-executions/:execId/stream
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_indexprior_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"
}

执行器实际做了什么

  1. 创建一行新的 app_execution,指向同一个应用版本。对于索引 0 .. start_from_stage_index - 1 的阶段日志,会立即以 status: 'skipped' 写入,运行历史会清楚地标明哪些阶段没有重新执行。
  2. 通过 dao.listArtifactsByStages(priorExecutionId, [stageIds]) 加载被跳过 阶段的产物。对于不携带内联 content 的产物,执行器会按 s3_key 从 S3 下载主体并按 UTF-8 解码,这样即使原始行已经卸载,下一阶段 的系统提示词也能看到之前的内容。
  3. 申请一个全新的沙箱。原执行沙箱上第 0-1 阶段写入的文件没有了,只有已持久化的 产物会穿越边界。如果后续阶段依赖的是中间临时文件而不是声明的产物,那么恢复就重现不 了它们。
  4. 构建第 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(提示词模糊、格式不对)。会让应用升一个新版本。

分享一次执行记录

你可以把一次执行的只读视图给队友看,适合"看看这个应用昨天产出了什么"这种场景,又不必 给对方应用本身的写权限。

  1. 打开运行。
  2. 点击分享
  3. 配置:包含哪些产物、有效期、是否允许下载、是否设密码门控。
  4. 获得一个公开链接: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" \
  -N

SSE 流会一直输出直到运行进入终止状态。或者轮询 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 也可通过定时任务的投递目标来支持。