mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-22 21:33:52 +08:00
Refine course progression and runtime safety
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
s01 → ... → s16 → [s17](../s17_integrated_harness/) → `s18` → [s19](../s19_goal_loop/)
|
||||
|
||||
> *"一次 tool_use,跑完一整套编排"* — `Workflow` 工具启动一个确定、可恢复的脚本运行时,批量派出去一堆子 agent。
|
||||
> *"一次 tool_use,跑完一整套编排"* — `Workflow` 工具启动一个确定、可恢复的脚本运行时,协调多次 agent 调用。
|
||||
>
|
||||
> **Harness 层**: 编排 — 在单 agent 循环之上,加一层确定的多 agent 脚本运行时。
|
||||
|
||||
@@ -22,7 +22,7 @@ s01 → ... → s16 → [s17](../s17_integrated_harness/) → `s18` → [s19](..
|
||||
|
||||
## 计划写在代码里,不是靠聊天一轮轮凑
|
||||
|
||||
在 harness 的工具池里加入一个 `Workflow` 工具。用户或模型给它一段脚本,脚本用 `agent() / parallel() / pipeline() / phase()` 这几个简单的原语,把编排写成确定的代码。
|
||||
在 harness 的工具池里加入一个 `Workflow` 工具。宿主注册由 `agent() / parallel() / pipeline() / phase()` 组成的可信脚本。模型只提供保存好的 workflow 名称、参数和可选的续跑 run ID,不会提交可执行代码或元数据。
|
||||
|
||||
主循环这边只看到一次 `tool_use`。脚本运行时,runtime 会不断发出生命周期和进度事件,并把每一步写进磁盘上的 journal。脚本结束后,这次调用返回启动信息、结果和任务状态。脚本里的中间结果存在变量里,不会塞进对话历史占地方。下次用 `resume_from_run_id` 重启时,没改过的 `agent()` 直接命中 journal 缓存,直接用之前的结果,断点续跑。
|
||||
|
||||
@@ -41,29 +41,41 @@ async def sample_workflow(ctx, args):
|
||||
|
||||
## Workflow 工具:一次调用,完成整次运行
|
||||
|
||||
`Workflow` 就在主 agent 的工具池里。用户可以要求运行一个保存好的 workflow,模型也可以在任务匹配已知编排时选择这个工具;两种情况最终都只发出一次 `Workflow(...)` 工具调用。
|
||||
`Workflow` 会加入 s17 宿主已有的工具池。用户可以要求运行一个保存好的 workflow,模型也可以在任务匹配已知编排时选择这个工具。适配器会用名称查询宿主管理的 `WORKFLOWS` registry,再把可信的元数据和函数交给运行时;s17 的其他工具仍在同一个循环里可用。
|
||||
|
||||
工具收到后会解析参数、校验 meta 信息、过权限检查、注册一个本地 workflow 任务,并在执行脚本前发出 `async_launched`。接下来依次发出进度事件和最终的 `task_notification`;调用返回启动信息、结果和任务状态。
|
||||
模型可见的 schema 只接受 `name`、`args` 和 `resume_from_run_id`。名称未知或参数格式错误时,适配器会返回错误工具结果,不会让宿主循环退出。随后运行时校验已经注册的元数据、经过权限检查、注册本地 workflow 任务,并在执行脚本前发出 `async_launched`。进度事件和最终的 `task_notification` 随后到达;调用返回可写入 JSON 的启动信息、结果和任务状态。
|
||||
|
||||
```python
|
||||
class WorkflowTool:
|
||||
async def call(self, meta, script_fn, args=None, resume_from_run_id=None):
|
||||
validate_meta(meta)
|
||||
check_permission(meta)
|
||||
run_id = resume_from_run_id or create_run_id(meta)
|
||||
task = LocalWorkflowTask(create_task_id(run_id), run_id, meta)
|
||||
task.event("async_launched", runId=run_id, taskId=task.task_id)
|
||||
...
|
||||
result = await script_fn(ctx, args)
|
||||
task.event("task_notification", status=task.status)
|
||||
return {"launched": launched, "result": result, "task": task}
|
||||
WORKFLOW_TOOL = {
|
||||
"name": "Workflow",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"args": {"type": "object"},
|
||||
"resume_from_run_id": {"type": "string"},
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
|
||||
async def run_workflow(name, args=None, resume_from_run_id=None):
|
||||
meta, script_fn = WORKFLOWS[name]
|
||||
out = await WorkflowTool().call(
|
||||
meta, script_fn,
|
||||
args=args,
|
||||
resume_from_run_id=resume_from_run_id,
|
||||
)
|
||||
return {"launched": out["launched"], "result": out["result"],
|
||||
"task": serialize_task(out["task"])}
|
||||
```
|
||||
|
||||
## Workflow 元数据:启动前先校验
|
||||
|
||||
每个 workflow 都要注册一个元数据对象,包含 `name`、`description` 和可选的 `phases`。运行时会在执行任何 workflow 代码之前校验它:`name` 和 `description` 用来标识任务,`phases` 给进度条分组命名。
|
||||
每个保存好的 workflow 都会注册一份可信元数据,包含 `name`、`description` 和可选的 `phases`。运行时会在执行 workflow 代码前校验它:`name` 和 `description` 用来标识任务,`phases` 给进度显示分组命名。这些字段属于宿主 registry,不是模型输入。
|
||||
|
||||
运行时在注册阶段直接拒绝错误输入并抛出 `WorkflowInputError`。这和 s14 校验 cron 表达式是一个思路:坏脚本别让它跑到执行的时候才炸。
|
||||
注册内容不合法时,运行时会在启动前抛出 `WorkflowInputError`。这和 s14 校验 cron 表达式是一个思路:保存好的 workflow 有问题,就不要等到执行时才发现。
|
||||
|
||||
运行时会把 `meta.name` 用在本地产物文件名中,因此还要求它是 1-64 个字符的安全 slug,只能包含字母、数字、`.`、`_`、`-`。
|
||||
|
||||
@@ -85,7 +97,7 @@ def validate_meta(meta):
|
||||
|
||||
## 编排原语:就这几个,够写所有流程
|
||||
|
||||
脚本跑在一个独立的上下文里,能用的全局变量就这几个编排原语。脚本本身不直接读写文件、不跑 shell,真正的代码操作都由派出去的子 agent 用它们自己的工具权限完成。这些原语都是 `ExecutionState` 上的方法:
|
||||
脚本收到一个只暴露少量编排原语的 `ExecutionState`,本身不直接读写文件,也不运行 shell。生产集成可以在 `agent()` 后接真实 agent runner,并保留 runner 自己的工具权限。本章使用 `MockAgentRunner`,让 journal 和续跑结果可以复现;示例中的审查发现是固定测试数据,不是真实代码审查结果。
|
||||
|
||||
| 原语 | 作用 |
|
||||
|------|------|
|
||||
@@ -140,7 +152,7 @@ class LocalWorkflowTask:
|
||||
|
||||
## 存储:快照 + journal,断了能续
|
||||
|
||||
运行时把每次运行的数据存在 `s18_workflow_runtime/.runtime/`:快照 `<runId>.json`、输出 `<runId>.output.json` 和 journal `<runId>.journal.jsonl`。快照与 journal 共享稳定的 `runId`,续跑时才能找到同一次运行的状态和已完成步骤。
|
||||
运行时把每次运行的数据存在 `s18_workflow_runtime/.runtime/`:快照 `<runId>.json`、输出 `<runId>.output.json`、journal `<runId>.journal.jsonl` 和协调文件 `<runId>.lock`。每次新运行都会在打开 journal 前,用排他式文件创建预留新的 `runId`。整次执行和最终持久化期间都持有 run lock,另一个进程不能同时 resume 同一次运行。快照记录 workflow 名称、参数和任务状态;resume 会先验证已保存的快照和 journal,再改动原有的成功产物。
|
||||
|
||||
journal 是断点续跑的核心,它一条一条记下来每个 `agent()` 的结果:
|
||||
|
||||
@@ -172,11 +184,11 @@ if cached is not MISS:
|
||||
|
||||
## 确定性:能复现,续跑才有意义
|
||||
|
||||
续跑要能工作,workflow 首先得可复现。稳定哈希和确定性的 runner 让同一份 workflow + 同样的参数产生同样的 key。因此 workflow 代码要避免不受控的时钟、随机数、文件系统状态等会让 key 在两次运行间变化的输入。
|
||||
续跑要能工作,workflow 首先得可复现。稳定哈希让同一份 workflow 和同样的参数产生同样的 journal key;本章的确定性 runner 还让示例结果保持一致。真实 runner 的内容可以变化,但语义调用 key 必须稳定,不能把不受控的时钟、随机数或文件系统状态混进 key。
|
||||
|
||||
## 跑起来看看
|
||||
|
||||
示例 workflow `review-changes`:用 `pipeline` 让每个审查维度独立走"审计 → 验证"流程。审计用一个带 schema 的 `agent()` 找问题,验证用 `parallel()` 给每条发现各派一个对抗性验证的子 agent,最后只留确认真实的问题,按严重度排序。
|
||||
示例 workflow `review-changes` 用 `pipeline` 让每个审查维度独立走“审计 → 验证”。确定性 runner 在审计阶段生成结构化测试发现,在验证阶段生成测试结论。这样示例只关注 pipeline、结构校验、journal 和续跑,不把课程结果绑在某个模型的审查质量上。
|
||||
|
||||
```python
|
||||
async def sample_workflow(ctx, args):
|
||||
@@ -206,24 +218,25 @@ async def sample_workflow(ctx, args):
|
||||
|--|-----------|---------------------|
|
||||
| 循环 | 单个、模型驱动 | 主循环不变;上面加一层确定的编排 |
|
||||
| 谁决定下一步 | 模型逐轮决定 | 脚本预先写好编排流程 |
|
||||
| 多 agent | s06 子 agent,一次性派出去 | 脚本化、可复现、可恢复的批量编排 |
|
||||
| 新增机制 | — | 脚本 DSL、任务生命周期、进度事件、journal/续跑、结构化输出、确定性 VM |
|
||||
| 多 agent | s06 子 agent,一次性派出去 | 通过 agent-runner 边界执行脚本化、可续跑的调用 |
|
||||
| 新增机制 | — | 编排原语、宿主 registry 与工具适配器、任务生命周期、进度事件、journal/续跑、结构化输出 |
|
||||
|
||||
s18 不替换主循环,它只是在工具层暴露了 `Workflow`,背后启动一个本地 workflow 运行时:一个 workflow 确定地驱动 N 个 agent 循环。s06 的子 agent 是模型临场派一次;s18 是把编排写成可以重放的脚本。
|
||||
s18 不替换主循环,它只是在工具层暴露 `Workflow`,背后启动一个本地 workflow 运行时:一份保存好的脚本通过 agent-runner 边界协调 N 次调用。s06 的子 agent 是模型临场派一次;s18 把编排写成可续跑的宿主代码。
|
||||
|
||||
## 试一下
|
||||
|
||||
```bash
|
||||
python s18_workflow_runtime/code.py # 启动 review-changes,看事件流
|
||||
python s18_workflow_runtime/code.py # 真实 API:模型可选择 Workflow 或任一 s17 工具
|
||||
python s18_workflow_runtime/code.py demo # 运行确定性的 review-changes 测试数据并观察事件流
|
||||
python s18_workflow_runtime/code.py resume # 用上次的 runId 续跑,每个 agent() 都命中 journal 缓存
|
||||
```
|
||||
|
||||
观察:一次启动 → `async_launched` → 阶段切换/子agent进度推进 → `task_notification`;结果存在任务对象上。续跑的时候会显示 `agents=0 tokens=0`(全部命中缓存),结果和上次一字不差。
|
||||
默认命令里,可以让模型运行保存好的 `review-changes` workflow;这次工具调用与继承自 s17 的工具走同一个循环和分发器。`demo` 命令直接运行确定性测试数据,便于重复观察生命周期和续跑。它会报告 11 次 runner 调用和 6 条测试发现;续跑时全部命中缓存,因此显示 `agents=0 tokens=0`。
|
||||
|
||||
## 接下来
|
||||
|
||||
编排是在 agent 能力之上又加了一层:主循环管单步操作,脚本管整支队伍的流程。把工作写成确定、可恢复的脚本,模型就从"逐轮驱动者"变成了"被脚本调度的执行单元"。同一个 `agent()`,既能在主循环里被模型临场调用,也能在 workflow 里被脚本批量编排。
|
||||
编排是在 agent 能力之上再加一层:主循环管单步操作,保存好的脚本管固定流程。本章让 agent-runner 边界保持确定;换成真实 runner 后,实际工作内容会改变,但 workflow 的生命周期、journal 和续跑约定不变。
|
||||
|
||||
下一章:[s19 Goal Loop](../s19_goal_loop/) — 编排把工作分派给多个 agent;下一章反过来,一个目标把控制权重拉回主循环,没达成就不让这一轮结束。
|
||||
下一章:[s19 Goal Loop](../s19_goal_loop/) — 编排把工作分派给多个 agent;下一章用一个聚焦的循环把控制权拉回目标。未达成时继续,达成或触发安全出口时把控制权交还用户。
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
<!-- translation-sync: zh@v6, en@v6, ja@v6 -->
|
||||
|
||||
Reference in New Issue
Block a user