agenttaskflow 0.0.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +262 -3
  2. package/dist/agent.d.ts +69 -0
  3. package/dist/agent.js +189 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +54 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/connection.d.ts +37 -0
  9. package/dist/connection.js +313 -0
  10. package/dist/connection.js.map +1 -0
  11. package/dist/errors.d.ts +14 -0
  12. package/dist/errors.js +15 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/events.d.ts +24 -0
  15. package/dist/events.js +60 -0
  16. package/dist/events.js.map +1 -0
  17. package/dist/flow.d.ts +10 -0
  18. package/dist/flow.js +32 -0
  19. package/dist/flow.js.map +1 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/loader.d.ts +1 -0
  24. package/dist/loader.js +41 -0
  25. package/dist/loader.js.map +1 -0
  26. package/dist/runner.d.ts +1 -0
  27. package/dist/runner.js +13 -0
  28. package/dist/runner.js.map +1 -0
  29. package/dist/state.d.ts +17 -0
  30. package/dist/state.js +92 -0
  31. package/dist/state.js.map +1 -0
  32. package/dist/sync.d.ts +31 -0
  33. package/dist/sync.js +151 -0
  34. package/dist/sync.js.map +1 -0
  35. package/dist/transport.d.ts +33 -0
  36. package/dist/transport.js +211 -0
  37. package/dist/transport.js.map +1 -0
  38. package/dist/worker.d.ts +1 -0
  39. package/dist/worker.js +55 -0
  40. package/dist/worker.js.map +1 -0
  41. package/docs/validation.md +27 -0
  42. package/docs/workflow-authoring.md +80 -0
  43. package/examples/common.ts +9 -0
  44. package/examples/multi-fields.ts +45 -0
  45. package/examples/review.ts +24 -0
  46. package/examples/simple.ts +16 -0
  47. package/examples/smoke.ts +41 -0
  48. package/package.json +19 -6
  49. package/test/README.md +37 -0
  50. package/test/RESULTS.md +27 -0
  51. package/test/branches.ts +23 -0
  52. package/test/context.ts +18 -0
  53. package/test/correction.ts +20 -0
  54. package/test/loop.ts +30 -0
  55. package/test/modules.ts +12 -0
  56. package/test/multi-fields.ts +22 -0
  57. package/test/run.ts +55 -0
  58. package/test/shared.ts +15 -0
  59. package/index.js +0 -1
@@ -0,0 +1,80 @@
1
+ # 编写 AgentTaskFlow 工作流
2
+
3
+ ## 使用正常 TypeScript
4
+
5
+ 直接写 `.ts`,用 tsx 或自己的 TypeScript 工具链执行。没有流程语法、解析器或 next/goto 调度器。默认 API 是同步的,不需要 async/await。
6
+
7
+ ```ts
8
+ import { flow } from 'agenttaskflow';
9
+ import { REVIEW, repair } from './common.js';
10
+
11
+ flow('cxh', step => {
12
+ // 先产生待审查的方案。
13
+ step('给出方案');
14
+
15
+ // 每轮按真实结果决定是否继续,不让模型操纵整个流程。
16
+ for (let round = 0; round < 3; round++) {
17
+ if (step.check(REVIEW)) break;
18
+ if (round === 2) throw new Error('审查仍未通过');
19
+ repair(step);
20
+ }
21
+ });
22
+ ```
23
+
24
+ 默认使用 `flow(command, step => { ... })`。`step(prompt)` 返回文本;`step.check(prompt)` 返回布尔值;`step.choose(prompt, ['pass', 'revise'])` 返回所选字符串。常见流程不需要类型声明或 schema。步骤名自动取提示词第一行,因此第一行应简短说明任务。
25
+
26
+ 共享模块接收现有 step,不要每步创建 flow。这样上下文和进程都连续。主流程每一阶段前必须有一句有意义的注释。需要自定义步骤名或 checkpoint 时使用完整的 Agent/withAgent API。
27
+
28
+ ## 提示词放在代码中
29
+
30
+ **所有提示词直接写在流程 `.ts` 或共享 `.ts` 模块的字符串变量/函数中。不要把各种文字拆成很多小文本文件,不要让 agent 每一步去读提示词文件。**
31
+
32
+ 共享模块例子:
33
+
34
+ ```ts
35
+ import type { Step } from 'agenttaskflow';
36
+
37
+ export const REVIEW = `检查上一轮成果是否满足最初的全部要求。`;
38
+ export const REPAIR = `根据上一轮意见修订,并报告真实修改。`;
39
+ export function repair(step: Step) {
40
+ return step(REPAIR);
41
+ }
42
+ ```
43
+
44
+ 只把完全相同的通用内容抽到 shared/common 模块,流程特有的提示词留在该流程文件。复用用普通 import,不设计额外 include 指令。
45
+
46
+ ## 结果和业务条件
47
+
48
+ 简单判断用 `step.check` 或 `step.choose`,库自动定义和校验结果格式。复杂结果才使用 `step.data(prompt, schema)`,用 Zod 的 `z.strictObject`、明确字段和枚举定义结构;也可以用完整 API 的 `agent.run(prompt, { schema })`。返回值已经解析,不需要手动 JSON.parse。模型输出不合法时库会在同一会话补答,默认最多两次。进程故障和权限拒绝不会当作格式错误重试。
49
+
50
+ 类型正确不代表业务合格。例如至少 15 个独立实验,应在 TypeScript 中维护 Map,以实验 ID 去重,验证证据再计数。循环要设定上限,耗尽后明确报错。不要只接受模型一句“已完成 15 个”。
51
+
52
+ 复杂条件使用 if/else、switch 或函数,不能用模型返回 next 字符串来决定任意跳转。模型可以返回业务事实或判定(pass/revise),真正的分支由代码决定。
53
+
54
+ ## 同步与异步
55
+
56
+ 默认使用 flow,step 同步返回,自动关闭进程。完整 API 的 Agent/withAgent、run 和 close 也同步。库内部的工作线程负责异步通信,实时输出不会停止。不需要额外写 main async 函数或 asyncio.run。
57
+
58
+ 若业务本身需要并行 agent 或嵌入服务器,可显式使用 AsyncAgent/withAsyncAgent。不要把 async 回调传给同步 flow/withAgent。普通 fetch 等 API 的异步语义保持原状。
59
+
60
+ 同步事件回调保持简短,不要重新调用当前 agent,不要依赖主线程 setTimeout 在 run 等待中触发任务控制。需要应用级 AbortSignal 时使用 AsyncAgent。
61
+
62
+ ## 中断和恢复
63
+
64
+ flow 和 withAgent 保证正常结束或异常时 close。需要 checkpoint 时使用 withAgent;显式 runId 和 workflowVersion 用于下次创建 Agent 恢复逻辑会话;loadCheckpoint/saveCheckpoint 记录业务阶段。checkpoint 输入应包含主题、提示词版本和关键参数。
65
+
66
+ 恢复会话不恢复 JavaScript 调用栈,也不自动跳过 run。重复调用就是重复发任务。先核实已有产物,避免中断时已产生的操作被重做。只有业务验证通过后才保存 checkpoint。
67
+
68
+ ## 长文档迁移
69
+
70
+ 如果要求“不遗漏、原文原样”,不要概括或删减指导文档。将完整内容放进 TS 多行字符串,按阶段引用,并建立原文到提示词和代码检查的对照。模板字符串里的反引号、反斜杠和 `${...}` 必须正确处理,确保运行时字符串与原文一致。
71
+
72
+ 每个分支、重试上限、合格条件和归档要求都要落实到普通 TypeScript 结构。不要把几千行全部一次发出去后宣称已经实现精确控制。
73
+
74
+ ## 测试
75
+
76
+ 先用假 CLI 检查分支、补答、超时、相同 PID 和上下文,再运行小型真实 smoke。不要直接用昂贵实验验证调度。所有流程输入输出默认可见,补答也会显示输入和次数。
77
+
78
+ ```bash
79
+ npm run smoke -- --provider codex --command cxh --rounds 5
80
+ ```
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+
3
+ // 提示词直接写在 TypeScript 模块中,复用使用普通 import。
4
+ export const REMEMBER = (token: string) => `记住标记 ${token},后续步骤会让你回忆。
5
+ 不要读写文件,不要调用工具。现在只回答“已记住”。`;
6
+ export const RECALL = (round: number) => `这是第 ${round} 轮测试。
7
+ 从当前对话回忆最开始给你的标记。不要读写文件,不要调用工具。
8
+ 返回 token 和 round 两个字段,round 必须是 ${round};不记得就返回 UNKNOWN,不要猜测。`;
9
+ export const Recall = z.strictObject({ token: z.string(), round: z.number().int() });
@@ -0,0 +1,45 @@
1
+ import { flow } from 'agenttaskflow';
2
+ import { z } from 'zod';
3
+
4
+ // 一次回答包含四个字段,库负责解析 JSON 和校验类型。
5
+ const Review = z.strictObject({
6
+ passed: z.boolean().describe('方案是否满足全部要求'),
7
+ score: z.number().int().min(0).max(100).describe('完整性评分,0 到 100'),
8
+ problems: z.array(z.string()).describe('未满足的要求;没有问题时返回空数组'),
9
+ suggestion: z.string().describe('具体修改建议;通过时说明无需修改'),
10
+ });
11
+
12
+ const REVIEW = `审查当前待办工具方案
13
+ 要求:支持添加任务、完成任务、列出任务、本地持久化,以及文件损坏时的错误处理。
14
+ 逐项核对当前方案,返回是否通过、评分、问题列表和修改建议。
15
+ 只有全部要求都满足时 passed 才能为 true。只审查方案,不读写文件,不执行实现。`;
16
+
17
+ flow('cxh', step => {
18
+ // 故意提供不完整的初稿,展示多个返回字段如何影响后续流程。
19
+ step(`记住待办工具初稿
20
+ 支持添加任务和列出任务,数据暂时保存在内存中。
21
+ 这只是初稿,请先原样复述,不要补全,不读写文件。`);
22
+
23
+ // 最多审查三轮;直接读取对象字段,不需要 JSON.parse。
24
+ for (let round = 1; round <= 3; round++) {
25
+ const { passed, score, problems, suggestion } = step.data(REVIEW, Review);
26
+
27
+ console.log(`\n第 ${round} 轮:通过=${passed},评分=${score}/100`);
28
+ console.log('问题列表:', problems);
29
+ console.log('修改建议:', suggestion);
30
+
31
+ // 同时检查通过标记和问题列表,避免带着未解决的问题继续。
32
+ if (passed && problems.length === 0) break;
33
+ if (round === 3) throw new Error('三轮审查后仍未通过,请检查上面的结果');
34
+
35
+ // 将本轮返回的多个字段用于下一步提示词,仍在同一会话中。
36
+ step(`修订待办工具方案
37
+ 本轮评分:${score}/100。
38
+ 问题列表:${JSON.stringify(problems)}。
39
+ 修改建议:${suggestion}
40
+ 请补全所有要求,并输出完整的新方案。只在对话里输出,不读写文件。`);
41
+ }
42
+
43
+ // 审查通过后汇总方案,避免把设计完成误报为代码实现完成。
44
+ step('总结最终通过审查的方案,并明确这只是设计方案,尚未实现代码。');
45
+ });
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import { withAgent } from 'agenttaskflow';
3
+
4
+ const DESIGN = `设计一个本地待办事项命令行工具。
5
+ 要求支持添加、完成、列表、持久化、错误处理和自动化测试。只给方案,不修改文件。`;
6
+ const REVIEW = `检查当前方案是否满足最初所有要求。返回 verdict=pass 或 revise,以及 reason。`;
7
+ const REVISE = `根据上一轮审查意见补全方案。只在对话中返回完整修订方案。`;
8
+ const Review = z.strictObject({ verdict: z.enum(['pass', 'revise']), reason: z.string() });
9
+
10
+ withAgent({ command: 'cxh' }, agent => {
11
+ // 建立方案。
12
+ agent.run(DESIGN, { name: '生成方案' });
13
+
14
+ // 审查通过后退出循环,否则修订,最多三轮。
15
+ for (let round = 1; round <= 3; round++) {
16
+ const review = agent.run(REVIEW, { name: `审查 ${round}`, schema: Review });
17
+ if (review.verdict === 'pass') break;
18
+ if (round === 3) throw new Error('三轮审查仍未通过');
19
+ agent.run(REVISE, { name: `修订 ${round}` });
20
+ }
21
+
22
+ // 前置检查通过后再输出汇总。
23
+ agent.run('总结最终方案和已修复的问题,不要声称已实现代码。', { name: '最终汇总' });
24
+ });
@@ -0,0 +1,16 @@
1
+ import { flow } from 'agenttaskflow';
2
+
3
+ flow('cxh', step => {
4
+ // 先生成方案,只在对话里工作,不修改文件。
5
+ step('设计一个本地待办工具,只给方案。要求支持添加、完成、列表、本地保存和错误处理。');
6
+
7
+ // 不合格就修订,最多检查三轮。
8
+ for (let i = 0; i < 3; i++) {
9
+ if (step.check('当前方案是否满足最初的全部要求?')) break;
10
+ if (i === 2) throw new Error('三轮后仍未通过');
11
+ step('根据刚才的检查补全方案。只在对话里输出,不修改文件。');
12
+ }
13
+
14
+ // 前置检查通过后汇总。
15
+ step('总结最终方案,不要声称已经实现代码。');
16
+ });
@@ -0,0 +1,41 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { withAgent, type Provider } from 'agenttaskflow';
4
+ import { REMEMBER, RECALL, Recall } from './common.js';
5
+
6
+ const { values } = parseArgs({ options: {
7
+ provider: { type: 'string', default: 'codex' }, command: { type: 'string' },
8
+ 'command-arg': { type: 'string', multiple: true },
9
+ cwd: { type: 'string', default: process.cwd() }, rounds: { type: 'string', default: '5' },
10
+ timeout: { type: 'string', default: '600' },
11
+ } });
12
+ const rounds = Number(values.rounds);
13
+ if (!Number.isInteger(rounds) || rounds < 1) throw new Error('--rounds 必须是正整数');
14
+ const token = `ATF-${randomUUID().replaceAll('-', '').slice(0, 12)}`;
15
+
16
+ try {
17
+ withAgent({
18
+ provider: values.provider as Provider,
19
+ command: [values.command ?? values.provider!, ...(values['command-arg'] ?? [])],
20
+ cwd: values.cwd, timeoutMs: Number(values.timeout) * 1000,
21
+ }, agent => {
22
+ // 先记住随机标记;后续输入不再包含标记,检查上下文是否连续。
23
+ agent.run(REMEMBER(token), { name: '记住标记' });
24
+ const pid = agent.pid;
25
+
26
+ // 使用普通 for/if 控制流程,每一轮都复用同一进程。
27
+ for (let round = 1; round <= rounds; round++) {
28
+ const result = agent.run(RECALL(round), { name: `回忆第 ${round} 轮`, schema: Recall });
29
+ if (result.token !== token || result.round !== round) throw new Error(`第 ${round} 轮上下文验证失败`);
30
+ if (!pid || agent.pid !== pid) throw new Error('常驻进程 PID 发生变化');
31
+ console.log(`PASS ${round}/${rounds} · PID=${pid}`);
32
+ }
33
+
34
+ // 业务自行保存检查点;不自动重放流程。
35
+ agent.saveCheckpoint('smoke', { rounds }, { token });
36
+ console.log(`完成 · runId=${agent.runId} · sessionId=${agent.sessionId}`);
37
+ });
38
+ } catch (error) {
39
+ console.error(String(error));
40
+ process.exitCode = 1;
41
+ }
package/package.json CHANGED
@@ -1,9 +1,22 @@
1
1
  {
2
2
  "name": "agenttaskflow",
3
- "version": "0.0.1",
4
- "description": "Persistent task workflows inside your existing Claude Code or Codex session (placeholder, coming soon)",
5
- "keywords": ["agent", "workflow", "claude-code", "codex", "llm"],
6
- "license": "MIT",
7
- "author": "maplewizard",
8
- "main": "index.js"
3
+ "version": "0.4.2",
4
+ "description": "TypeScript workflows with persistent Codex and Claude Code sessions",
5
+ "type": "module",
6
+ "engines": { "node": ">=22.15.0" },
7
+ "bin": { "atf": "dist/cli.js" },
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
+ "files": ["dist", "examples", "test", "docs", "README.md"],
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "check": "tsc --noEmit -p tsconfig.check.json",
13
+ "test": "npm run build && node --import tsx --test --test-concurrency=1 tests/*.test.ts",
14
+ "smoke": "tsx examples/smoke.ts",
15
+ "simple": "tsx examples/simple.ts",
16
+ "multi-fields": "tsx examples/multi-fields.ts",
17
+ "flows": "tsx test/run.ts",
18
+ "prepack": "npm run build"
19
+ },
20
+ "dependencies": { "esbuild": "~0.28.0", "proper-lockfile": "^4.1.2", "zod": "^4.0.0" },
21
+ "devDependencies": { "@types/node": "^22.0.0", "@types/proper-lockfile": "^4.1.4", "tsx": "^4.20.0", "typescript": "^5.9.0" }
9
22
  }
package/test/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # 真实 agent 流程测试
2
+
3
+ 最近一次真实 cxh 测试:六项全部通过,详细耗时和会话信息见 [RESULTS.md](./RESULTS.md)。
4
+
5
+ 在项目根目录运行,默认使用 cxh。每个测试是普通 TS 函数,提示词直接放在 TS 中,公共逻辑放在 shared.ts。统一入口负责参数、会话和测试汇总。
6
+
7
+ ```bash
8
+ npm run flows -- --list
9
+ npm run flows -- multi-fields
10
+ npm run flows -- branches loop
11
+ npm run flows -- all
12
+ ```
13
+
14
+ | 流程 | 检查内容 | 正常步骤数(不含补答) |
15
+ | --- | --- | --- |
16
+ | multi-fields | 五个字段,含数组和嵌套对象,校验订单计算 | 1 |
17
+ | branches | if/else 两个分支、choose 和 switch | 6 |
18
+ | loop | while 循环,按返回的缺失项逐项修订 | 7 |
19
+ | modules | import 公共函数,同一会话中更新购物清单 | 4 |
20
+ | context | 随机标记跨五轮回忆,核对轮数 | 6 |
21
+ | correction | 本地校验故意拒绝首次结果,验证补答 prompt | 1 + 至少一次补答 |
22
+
23
+ 指定其他命令或 provider:
24
+
25
+ ```bash
26
+ npm run flows -- all --command cx
27
+ npm run flows -- multi-fields --provider claude --command claude
28
+ npx tsx test/run.ts context --command cxh --timeout 600
29
+ ```
30
+
31
+ `--command` 是可执行文件,不是 shell 字符串。固定参数使用重复的 `--command-arg`。`--timeout` 是每个步骤的总超时秒数,默认 600。
32
+
33
+ 每个测试启动独立 agent 会话,测试内多步共用进程和上下文。因此 all 会依次启动六个会话,cxh 的启动/预热也会执行六次。测试调用真实模型,会消耗额度;只要求对话计算和设计,不要求读写业务文件。运行日志仍保存在项目的 .agenttaskflow/。
34
+
35
+ 通过与否由 TS 断言决定,不靠模型自己声称成功。某个测试失败后继续执行其余测试,最后以非零退出码表示存在失败。correction 专门使用故障注入验证补答,其他测试仍可能因模型返回不正确而失败。
36
+
37
+ 这里的 test/ 用于人工运行真实 agent 流程;tests/ 是已有的自动回归测试(npm test),使用假 CLI 验证协议、超时和清理等行为。
@@ -0,0 +1,27 @@
1
+ # 真实流程测试结果
2
+
3
+ 日期:2026-09-26。环境:macOS、Node.js 22.19.0,provider 为 codex,命令为 cxh,包装脚本报告模型 deepseek-v4-flash。
4
+
5
+ 执行:`node --import tsx test/run.ts all --command cxh`(使用 Node.js 22.19.0 的绝对路径)。
6
+
7
+ 结果:**6 通过,0 失败,退出码 0**。累计约 195 秒,包含各测试启动及预热。
8
+
9
+ | 流程 | 耗时 | PID | 补答 | 核对结果 |
10
+ | --- | --- | --- | --- | --- |
11
+ | multi-fields | 13.1s | 17324 | 0 | 五个字段及嵌套对象匹配,订单总价 11,余额 1 |
12
+ | branches | 40.3s | 22748 | 0 | 库存 0 判 false、库存 3 判 true,枚举选择 pickup |
13
+ | loop | 53.4s | 38505 | 0 | 四轮审查,缺失项依次为 3、2、1、0,三次补充 |
14
+ | modules | 28.2s | 59310 | 0 | 共享模块首次返回 3 件/11 元,更新后返回 4 件/16 元 |
15
+ | context | 38.4s | 69992 | 0 | 五轮回忆均返回正确随机标记与轮数 |
16
+ | correction | 21.4s | 85239 | 1 | 首次本地校验故意拒绝,补答后返回 42 件,验证调用两次 |
17
+
18
+ 每个流程只有一个 process_started 事件,后续步骤及补答没有重新启动进程。会话 ID:
19
+
20
+ - multi-fields:01a0de5f-a328-7171-981b-861590b0117f
21
+ - branches:01a0de5f-d47a-7d51-9517-4b545678cc3e
22
+ - loop:01a0de60-7007-70a0-a072-1cdcfea7eda9
23
+ - modules:01a0de61-4020-7241-92d3-59f37d1dd194
24
+ - context:01a0de61-afaa-72b1-8716-401923e60d4d
25
+ - correction:01a0de62-475e-76b3-88e0-a337ecb42e1c
26
+
27
+ 测试核对的是结构化结果和流程走向,不评估所有自由文本的事实准确性。例如 branches 中的通知是模型生成的示例文案,不代表实际发生发货。correction 使用故障注入验证补答链路,本次没有自然出现 JSON 格式错误。本次六个流程只运行了 cxh,未运行 Claude Code。
@@ -0,0 +1,23 @@
1
+ import assert from 'node:assert/strict';
2
+ import type { Step } from 'agenttaskflow';
3
+ import { CHAT_ONLY } from './shared.js';
4
+
5
+ export default function branches(step: Step) {
6
+ // 强制覆盖 if 和 else 两条路径,条件有可核实的标准答案。
7
+ for (const stock of [0, 3]) {
8
+ const available = step.check(`检查库存\n当前库存 ${stock} 件,是否能立即交付 2 件?${CHAT_ONLY}`);
9
+ assert.equal(available, stock >= 2);
10
+ if (available) step(`库存充足\n请给出一条发货通知。${CHAT_ONLY}`);
11
+ else step(`库存不足\n请给出一条缺货通知,不要声称已经发货。${CHAT_ONLY}`);
12
+ console.log(`PASS 分支:${available ? '发货' : '缺货'}`);
13
+ }
14
+
15
+ // 三选一返回枚举,由普通 switch 决定走向。
16
+ const choice = step.choose(`选择运输方式\n当天送达选 express,普通配送选 standard,自取选 pickup。
17
+ 客户要求自取,应选择哪个?${CHAT_ONLY}`, ['express', 'standard', 'pickup']);
18
+ assert.equal(choice, 'pickup');
19
+ switch (choice) {
20
+ case 'pickup': step(`自取分支\n请写一条到店取货提醒。${CHAT_ONLY}`); break;
21
+ default: throw new Error(`走错分支:${choice}`);
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import assert from 'node:assert/strict';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import type { Step } from 'agenttaskflow';
5
+ import { CHAT_ONLY } from './shared.js';
6
+
7
+ export default function context(step: Step) {
8
+ // 后续提示词不再包含随机标记,实际检查上下文是否保留。
9
+ const token = randomUUID();
10
+ step(`记住随机标记\n标记为 ${token}。之后的回答必须原样回忆它。${CHAT_ONLY}`);
11
+ for (let round = 1; round <= 5; round++) {
12
+ const result = step.data(`回忆第 ${round} 轮\n返回最初的标记 token,以及当前轮数 round。`,
13
+ z.strictObject({ token: z.string(), round: z.number().int() }));
14
+ assert.equal(result.token, token);
15
+ assert.equal(result.round, round);
16
+ console.log(`PASS 回忆 ${round}/5`);
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict';
2
+ import { z } from 'zod';
3
+ import type { Step } from 'agenttaskflow';
4
+ import { CHAT_ONLY } from './shared.js';
5
+
6
+ export default function correction(step: Step) {
7
+ // 故障注入:故意拒绝首次结构正确的回答,确定性地验证补答链路。
8
+ // 这不是日常业务写法,也不代表模型首次输出一定有格式问题。
9
+ let validations = 0;
10
+ const Result = z.strictObject({ answer: z.number().int(), unit: z.string() })
11
+ .superRefine((_value, context) => {
12
+ if (++validations === 1) context.addIssue({ code: 'custom',
13
+ message: '测试注入:首次结果不予接受。请沿用已经算出的结果,仅重新提交完整 JSON,不重复任务。' });
14
+ });
15
+
16
+ const result = step.data(`计算总件数\n每箱 6 件,共 7 箱。返回 answer 和 unit,unit 必须为“件”。${CHAT_ONLY}`, Result);
17
+ assert.ok(validations >= 2, '应至少发生一次补答');
18
+ assert.deepEqual(result, { answer: 42, unit: '件' });
19
+ console.log(`PASS 补答校验,验证次数=${validations}`);
20
+ }
package/test/loop.ts ADDED
@@ -0,0 +1,30 @@
1
+ import assert from 'node:assert/strict';
2
+ import { z } from 'zod';
3
+ import type { Step } from 'agenttaskflow';
4
+ import { CHAT_ONLY } from './shared.js';
5
+
6
+ export default function loop(step: Step) {
7
+ // 故意从不完整方案开始,每轮只增加一项,保证循环多次执行。
8
+ const required = ['添加任务', '完成任务', '本地保存'];
9
+ const features: string[] = [];
10
+ let reviews = 0;
11
+ while (reviews < 4) {
12
+ const result = step.data(`审查第 ${reviews + 1} 轮\n要求:${JSON.stringify(required)}。
13
+ 当前方案仅包含:${JSON.stringify(features)}。不要自行补全。
14
+ 按要求的原始顺序,返回缺失项 missing,全部具备时 passed 为 true。${CHAT_ONLY}`,
15
+ z.strictObject({ passed: z.boolean(), missing: z.array(z.string()) }));
16
+ reviews++;
17
+ const expected = required.filter(feature => !features.includes(feature));
18
+ assert.deepEqual(result.missing, expected);
19
+ assert.equal(result.passed, expected.length === 0);
20
+ console.log(`第 ${reviews} 轮,缺失项:`, result.missing);
21
+ if (result.passed) break;
22
+
23
+ // 使用返回数组来决定本轮修订内容,流程状态由 TS 维护。
24
+ const addition = result.missing[0];
25
+ step(`补充一项设计\n请描述“${addition}”的设计,只补充这一项。${CHAT_ONLY}`);
26
+ features.push(addition);
27
+ }
28
+ assert.equal(reviews, 4);
29
+ assert.deepEqual(features, required);
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { Step } from 'agenttaskflow';
2
+ import { CHAT_ONLY, summarizeCart, assertTotals } from './shared.js';
3
+
4
+ export default function modules(step: Step) {
5
+ // 主流程写入上下文,公共模块读取同一个会话里的清单。
6
+ step(`记住购物清单\n苹果单价 3 元,数量 2。梨单价 5 元,数量 1。${CHAT_ONLY}`);
7
+ assertTotals(summarizeCart(step), 3, 11);
8
+
9
+ // 修改上下文后再次调用同一个公共函数。
10
+ step(`更新购物清单\n再加 1 个梨,其他不变。${CHAT_ONLY}`);
11
+ assertTotals(summarizeCart(step), 4, 16);
12
+ }
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict';
2
+ import { z } from 'zod';
3
+ import type { Step } from 'agenttaskflow';
4
+ import { CHAT_ONLY } from './shared.js';
5
+
6
+ export default function multiFields(step: Step) {
7
+ // 一次返回字符串、数字、布尔值、数组和嵌套对象。
8
+ const result = step.data(`计算订单\n订单号 ORDER-42,苹果单价 3 元买 2 件,梨单价 5 元买 1 件。
9
+ 预算 12 元。按输入顺序返回商品名称,计算是否在预算内和剩余额度。${CHAT_ONLY}`,
10
+ z.strictObject({
11
+ orderId: z.string(),
12
+ total: z.number(),
13
+ withinBudget: z.boolean(),
14
+ items: z.array(z.string()),
15
+ budget: z.strictObject({ limit: z.number(), remaining: z.number() }),
16
+ }));
17
+ console.log('解析后的对象:', result);
18
+ assert.deepEqual(result, {
19
+ orderId: 'ORDER-42', total: 11, withinBudget: true,
20
+ items: ['苹果', '梨'], budget: { limit: 12, remaining: 1 },
21
+ });
22
+ }
package/test/run.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { flow, type Step } from 'agenttaskflow';
3
+ import context from './context.js';
4
+ import multiFields from './multi-fields.js';
5
+ import branches from './branches.js';
6
+ import loop from './loop.js';
7
+ import modules from './modules.js';
8
+ import correction from './correction.js';
9
+
10
+ const cases: Record<string, (step: Step) => void> = {
11
+ 'multi-fields': multiFields, branches, loop, modules, context, correction,
12
+ };
13
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
14
+ provider: { type: 'string', default: 'codex' },
15
+ command: { type: 'string' },
16
+ 'command-arg': { type: 'string', multiple: true },
17
+ timeout: { type: 'string', default: '600' },
18
+ list: { type: 'boolean', default: false },
19
+ } });
20
+ if (values.list) {
21
+ console.log('可用流程:\n' + Object.keys(cases).join('\n') + '\nall');
22
+ } else {
23
+ if (!['codex', 'claude'].includes(values.provider!)) throw new Error('provider 必须是 codex 或 claude');
24
+ const timeout = Number(values.timeout);
25
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('timeout 必须是正数(秒)');
26
+ const selected = positionals.length ? positionals : ['multi-fields'];
27
+ const names = selected.includes('all') ? Object.keys(cases) : [...new Set(selected)];
28
+ for (const name of selected) if (name !== 'all' && !Object.hasOwn(cases, name)) throw new Error(`未知流程:${name},使用 --list 查看`);
29
+ let failures = 0;
30
+ for (const name of names) {
31
+ console.log(`\n========== 测试:${name} ==========`);
32
+ const started = Date.now();
33
+ const pids = new Set<unknown>();
34
+ let corrections = 0;
35
+ try {
36
+ // 每个测试独立会话;一个测试内部的全部步骤复用同一进程。
37
+ flow({ provider: values.provider as 'codex' | 'claude',
38
+ command: [values.command ?? (values.provider === 'claude' ? 'claude' : 'cxh'), ...(values['command-arg'] ?? [])],
39
+ timeoutMs: timeout * 1000,
40
+ onEvent: event => {
41
+ if (event.kind === 'process_started') pids.add(event.text);
42
+ if (event.kind === 'correction') corrections++;
43
+ },
44
+ }, cases[name]);
45
+ if (pids.size !== 1) throw new Error(`预期只启动一个进程,实际 ${pids.size}`);
46
+ if (name === 'correction' && corrections < 1) throw new Error('没有收到补答事件');
47
+ console.log(`PASS ${name} · ${((Date.now() - started) / 1000).toFixed(1)}s · 补答 ${corrections} 次`);
48
+ } catch (error) {
49
+ failures++;
50
+ console.error(`FAIL ${name}:`, error);
51
+ }
52
+ }
53
+ console.log(`\n测试结束:${names.length - failures} 通过,${failures} 失败`);
54
+ if (failures) process.exitCode = 1;
55
+ }
package/test/shared.ts ADDED
@@ -0,0 +1,15 @@
1
+ import assert from 'node:assert/strict';
2
+ import { z } from 'zod';
3
+ import type { Step } from 'agenttaskflow';
4
+
5
+ // 公共提示词直接放在 TS 中,多个流程复用同一个 step 和会话。
6
+ export const CHAT_ONLY = '只在对话中完成任务,不读写文件,不调用外部工具。';
7
+ export const Totals = z.strictObject({ count: z.number().int(), total: z.number() });
8
+
9
+ export function summarizeCart(step: Step) {
10
+ return step.data(`汇总购物清单\n根据之前记住的清单,返回商品总件数 count 和总价 total。${CHAT_ONLY}`, Totals);
11
+ }
12
+
13
+ export function assertTotals(result: z.infer<typeof Totals>, count: number, total: number) {
14
+ assert.deepEqual(result, { count, total });
15
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};