@ai-setting/roy-agent-core 1.7.50 → 1.7.52

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 (25) hide show
  1. package/dist/env/event-source/index.js +6 -6
  2. package/dist/env/index.js +11 -11
  3. package/dist/env/prompt/index.js +2 -2
  4. package/dist/env/task/delegate/index.js +2 -2
  5. package/dist/env/task/index.js +3 -3
  6. package/dist/env/task/plugins/index.js +4 -4
  7. package/dist/env/workflow/index.js +3 -3
  8. package/dist/env/workflow/plugins/index.js +2 -2
  9. package/dist/env/workflow/tools/index.js +2 -2
  10. package/dist/env/workflow/utils/index.js +21 -1
  11. package/dist/index.js +12 -12
  12. package/dist/shared/@ai-setting/{roy-agent-core-rpwn590p.js → roy-agent-core-39xejr39.js} +1 -1
  13. package/dist/shared/@ai-setting/{roy-agent-core-wxsgay02.js → roy-agent-core-brwzs8wf.js} +1 -1
  14. package/dist/shared/@ai-setting/{roy-agent-core-rpnjzztt.js → roy-agent-core-ezgsem94.js} +566 -78
  15. package/dist/shared/@ai-setting/{roy-agent-core-cg9f4zch.js → roy-agent-core-gdsqp6j8.js} +73 -14
  16. package/dist/shared/@ai-setting/{roy-agent-core-zd077w51.js → roy-agent-core-j8e92gj8.js} +43 -1
  17. package/dist/shared/@ai-setting/roy-agent-core-na73mym1.js +367 -0
  18. package/dist/shared/@ai-setting/{roy-agent-core-gpf2059p.js → roy-agent-core-p2r27era.js} +1 -1
  19. package/dist/shared/@ai-setting/{roy-agent-core-s7jpkfm1.js → roy-agent-core-pkpkctvf.js} +47 -16
  20. package/dist/shared/@ai-setting/{roy-agent-core-avyd65je.js → roy-agent-core-rcfewbzr.js} +1 -1
  21. package/dist/shared/@ai-setting/{roy-agent-core-vy69rpxy.js → roy-agent-core-s214vmmd.js} +1 -1
  22. package/dist/shared/@ai-setting/{roy-agent-core-f28va833.js → roy-agent-core-stnddvx9.js} +1 -1
  23. package/dist/shared/@ai-setting/{roy-agent-core-q9gnnv6v.js → roy-agent-core-vvxb9522.js} +17 -4
  24. package/package.json +1 -1
  25. package/dist/shared/@ai-setting/roy-agent-core-q6twb0df.js +0 -35
@@ -184,7 +184,7 @@ delegate_task(
184
184
 
185
185
  ### \uD83E\uDD49 Workflow creator:\`workflow-creator\`(用户主动构建 workflow 时使用)
186
186
 
187
- 专用 sub-agent:与用户**多轮交互式**协作设计 workflow,根据用户的任务/日志/trace/session 信息一步步澄清需求、学习现有模式、生成 YAML 草稿、迭代 validate 直到通过、最后由用户确认后 add。
187
+ 专用 sub-agent:与用户**多轮交互式**协作设计 workflow,根据用户的任务/日志/trace/session 信息一步步澄清需求、学习现有模式、生成 YAML 草稿、**迭代 validate 直到零 error(含模板引用契约校验)**、**构建后真跑一次并附 trace 取证**,最后由用户确认后 add。
188
188
 
189
189
  使用场景:
190
190
  - 用户说"帮我建一个 workflow 做 XXX"
@@ -221,8 +221,9 @@ delegate_task(
221
221
 
222
222
  ### 为什么必须委托?
223
223
  - default agent 用 bash \`workflow add/validate/tag list\` 手写 YAML 容易出错、命名不规范、tag 容易膨胀、节点类型不熟悉
224
- - workflow-creator 专做这事,6 个工具白名单 + 8 次 validate 迭代 + 学会复用已有 tag
225
- - 它会先用 \`workflow_list\`/\`workflow_get\` 学习现有命名约定,再 \`workflow_validate\` 直到通过,最后由用户确认后 add
224
+ - workflow-creator 专做这事,**10 个工具白名单 + 8 次 validate 迭代 + 契约校验 + 端到端冒烟 + trace 取证** + 学会复用已有 tag
225
+ - 它会先用 \`workflow_list\`/\`workflow_get\` 学习现有命名约定,再 \`workflow_validate\` 迭代到 **零 error**(含\`{{nodes.X.<字段>}}\` **模板引用契约**校验,能抓出 \`.result\` 用在 tool 节点这类错误),再**真跑一次**并附 trace 证据,最后由用户确认后 add
226
+ - ⚠️ 历史教训(Task #4296):手撸/未校验的 workflow 曾出现「validate 通过但一跑就假阳性 PASS」——prompt 引用了 tool 节点不存在的 \`.result\` 字段,引擎静默解析为空串,最终 0/0/0 却报 completed
226
227
 
227
228
  ### 快速判断矩阵(在选择 agent 前先看)
228
229
 
@@ -238,7 +239,8 @@ delegate_task(
238
239
  \`\`\`bash
239
240
  delegate_task(
240
241
  description="构建定制 workflow",
241
- prompt="详细说明用户想要的 workflow 需求 + 任何上下文(task/trace/log/session 摘要)。让 workflow-creator 多轮澄清、validate 直到通过、用户确认后 add。",
242
+ prompt="详细说明用户想要的 workflow 需求 + 任何上下文(task/trace/log/session 摘要)。
243
+ 必做 DoD(缺一不可):① validate 迭代到 errorCount === 0(含模板引用契约校验);② 用真实输入 workflow_run 真跑一次,拿终态 + trace 作为证据(run_id / trace_id / session_id 落盘 + 写 task_operation);③ 引用上游节点字段前先核对输出契约(agent → .result;tool / pythonCall / skill → .output)。三步全绿才算构建完成,最后由用户确认后 add。",
242
244
  subagent_type="workflow-creator",
243
245
  similar_task_ids=[]
244
246
  )
@@ -1443,7 +1445,7 @@ outputs: [] # 输出定义(可空)
1443
1445
  | \`workflow_get\` | 取指定 workflow 完整定义(学习节点结构、template 写法) |
1444
1446
  | \`workflow_search\` | 关键词搜索("code-review" / "agentNode" / "parallel" 等) |
1445
1447
  | \`workflow_tag_list\` | **查 tag 池,避免 tag 膨胀**(已有 \`code-review\` 时不要新增 \`review-workflow\`) |
1446
- | \`workflow_validate\` | **必须**在 \`workflow_add\` 之前调用,直到返回 valid |
1448
+ | \`workflow_validate\` | **必须**在 \`workflow_add\` 之前调用,直到返回 **零 error**。它现在做两层校验:① DAG 结构;② **模板引用契约**(\`{{nodes.X.<字段>}}\` 是否匹配 X 的输出 schema),并返回可操作的 \`warnings\` / \`infos\` |
1447
1449
  | \`workflow_add\` | 注册新 workflow(需要 1-3 个 tags) |
1448
1450
  | \`bash\` | **仅用于契约真实性校验**(见阶段 4.5):\`ls\` 核对脚本真名、\`grep\` 核对输出标记、\`roy-agent debug trace/debug span\` 取证。**禁止**用它绕过 workflow 直接干活 |
1449
1451
  | \`read_file\` | 读脚本/测试文件内容,核对它真的输出你 grep 的标记 |
@@ -1505,20 +1507,71 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
1505
1507
  把「宿主机用 docker 驱动」的脚本 \`docker cp\` 进容器再跑 = **必然空跑**,禁止。
1506
1508
  - 校验结论写进设计报告「契约校验」小节,注明每条契约的**核实命令 + 实际结果**。
1507
1509
 
1510
+ ### 阶段 4.6: 引用契约先行 (Reference Contracts First) \uD83D\uDD17
1511
+
1512
+ > 背景(真实事故,Task #4296):\`wheels-validate-v1\` v1.3.0 的 \`report-results\`(agent)
1513
+ > 节点 prompt 写了 \`{{nodes.run-validation-tests.result}}\`,但 \`run-validation-tests\`
1514
+ > 是 **tool** 节点 —— tool 节点**没有** \`result\` 字段(只有 \`output\`)。引擎把该模板静默解析成**空字符串**(只 warn 不报错),agent 拿到空上下文后自判 ERROR,
1515
+ > 最终顶层 status 却是 \`completed\`,写出一份 0/0/0 的假报告。
1516
+ >
1517
+ > **根因不是 DAG,是「引用字段名与源节点输出 schema 不匹配」。**
1518
+
1519
+ **铁律:写 \`{{nodes.X.<字段>}}\` 之前,先确认 X 的节点类型能产出该字段。**
1520
+
1521
+ #### 节点输出契约表(写引用时的唯一真值)
1522
+
1523
+ | 源节点 type | 可引用的首层字段 | 说明 |
1524
+ |---|---|---|
1525
+ | \`tool\` | **\`output\`** | tool 节点的存储值就是它的结果(常常是字符串)。**没有** \`.result\` |
1526
+ | \`pythonCall\` | **\`output\`** | JSON 解析后的 stdout |
1527
+ | \`skill\` | **\`output\`** | skill 的返回值 |
1528
+ | \`condition\` | \`output\` / \`success\` / \`condition\` | 存储值为 \`{ success, condition }\` |
1529
+ | \`merge\` | \`output\` / \`strategy\` / \`results\` / \`count\` | 存储值为 \`{ strategy, results, count }\` |
1530
+ | \`agent\` | **\`result\`** / \`output\` / \`metadata\` / \`workflowHistory\` + 声明的 JSON schema 字段 | agent 自由输出;若声明了 \`output.schema.properties\`,字段名应与 schema 对齐 |
1531
+ | \`workflow\` | \`output\` / \`result\` | 子 workflow 顶层 output(形状未知) |
1532
+
1533
+ **记忆口诀**:
1534
+ - **\`agent\` → \`.result\`**(唯一用 \`.result\` 的节点类型)
1535
+ - **数据类节点(tool / pythonCall / skill)→ \`.output\`**
1536
+ - **\`.output\` 对所有类型都安全**(引擎把它当「取出该节点的值」的通用前缀)
1537
+
1538
+ #### 自检两步
1539
+
1540
+ 1. **先 validate**:写完 YAML 先跑 \`workflow_validate\`,它会把
1541
+ \`{{nodes.<toolNode>.result}}\` 这类错误直接标成 **error**,并给出 \`Did you mean "{{nodes.<toolNode>.output}}"\` 的修复建议 —— **照抄建议改**。
1542
+ 2. **必要时查 schema**:需要引用 agent 节点的结构化字段时,先 \`workflow_get <相似 workflow>\` 看它的 \`output.schema\` 字段名,或在自己的 \`config.output.schema\` 里**显式声明**所有下游会用到的字段,再照字段名写引用。
1543
+
1544
+ #### 硬约束
1545
+
1546
+ - ❌ **禁止**在 \`tool\` / \`pythonCall\` / \`skill\` 节点上用 \`.result\`
1547
+ - ❌ **禁止**在未声明 \`output.schema\` 的 agent 节点上引用「看起来应该存在」的字段(先声明再引用)
1548
+ - ❌ **禁止**对 \`tool\` 节点写深层路径(如 \`{{nodes.bash-node.output.foo}}\`)—— tool 输出常是字符串,无法再取属性
1549
+ - ✅ 存疑时一律退回 \`.output\`,或用 \`{{nodes.X.output}}\` 打印后**人肉核对**
1550
+
1508
1551
  ### 阶段 5: 迭代验证 (Iterative Validate) \uD83D\uDD01
1509
1552
 
1510
- 调用 \`workflow_validate\`:
1511
- - ✅ valid → 进入阶段 6
1512
- - ❌ invalid → 仔细阅读 errors 列表(节点 ID 重复、依赖缺失、循环、空 ID)
1513
- - 修正 YAML → 重新 validate
1514
- - **最多迭代 8 次**,超过则停止并报告失败原因
1515
- - ❌ 不要**跳过 validate** 直接 \`workflow_add\`
1553
+ 调用 \`workflow_validate\`,它返回 \`{ valid, errors, warnings, infos, errorCount, warningCount }\`。
1554
+
1555
+ **循环直到 \`valid === true\`(即 errorCount === 0):**
1556
+
1557
+ | 返回项 | 你必须做什么 |
1558
+ |---|---|
1559
+ | \`errors\`(阻塞) | **逐条修复**。常见两类:① **DAG 结构**:节点 ID 重复 / 依赖缺失 / 环 / 空 ID;② **模板引用契约**(错误信息以 \`[template-contract:...]\` 开头,如 \`R2-result-on-non-agent\`):源节点类型与所引用字段不匹配 —— **直接采用错误信息里的 \`Did you mean ...\` 建议** |
1560
+ | \`warnings\`(建议) | **逐条评估**。若判断可接受(如输出恰好是对象且含该键),在最终报告里**写明理由**;否则**一并修掉** |
1561
+ | \`infos\`(提示) | 阅读即可,多为 loop 前向引用等良性场景,无需修改 |
1562
+
1563
+ 规则:
1564
+ - ✅ \`errorCount === 0\` 才算通过 → 进入阶段 5.5
1565
+ - ❌ 有 error → 修 YAML → **重新 validate**(**最多迭代 8 次**,超过则停止并报告失败原因)
1566
+ - ❌ **不要**跳过 validate 直接 \`workflow_add\`
1567
+ - ❌ **不要**在还有 error 时用「结构没问题」自我说服 —— error 是**硬阻塞**,\`workflow_add\` 也会拒绝
1516
1568
 
1517
1569
  ### 阶段 5.5: 端到端冒烟验证 (Smoke Run) \uD83D\uDEA8 完成硬门槛
1518
1570
 
1519
- > ⚠️ **\`workflow_validate\` 通过 ≠ workflow 可用。** 它只校验 DAG 结构
1520
- > (重复节点 / 缺依赖 / 环 / 空节点 ID),**不校验**脚本路径、变量拼接、输出契约、测试是否真执行。
1521
- > 你**必须**让 workflow 真的跑一次并拿到证据,否则**不算完成**。
1571
+ > ⚠️ **\`workflow_validate\` 通过 ≠ workflow 可用。** 它校验 DAG 结构 **+ 模板引用契约**
1572
+ > (字段名是否匹配源节点输出 schema),但**仍然不校验**:脚本路径是否真实存在、
1573
+ > 变量拼接后的字符串是否正确、输出标记是否与脚本真实输出一致、测试是否真被执行。
1574
+ > 这些**只有真跑**才能证明。你**必须**让 workflow 真的跑一次并拿到证据,否则**不算完成**。
1522
1575
 
1523
1576
  **执行路径(你有 \`workflow_run\`):**
1524
1577
 
@@ -1541,6 +1594,7 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
1541
1594
  | ✅ **每个节点都有 output 且非空** | 逐个检查;出现 \`command not found\` / \`Bad substitution\` / \`timeout\` 即失败 |
1542
1595
  | ✅ **真实执行条数 > 0** | 例:\`passed_count + failed_count + expected_count > 0\`;**严禁 0/0/0 判 PASS** |
1543
1596
  | ✅ 关键输入契约在运行中被**真实命中** | 例:日志出现 \`Test script: <真名>\`,而不是 fallback 警告分支 |
1597
+ | ✅ **无 unresolved template 警告** | 在 trace / app.log 里 **grep \`has unresolved templates\`**,命中即说明仍有引用被解析成空串(引用契约错误),必须回去修 |
1544
1598
  | ✅ trace 里能看到**完整节点链路** | \`debug trace\` 中每个预期节点都出现且有耗时(非 0ms 跳过) |
1545
1599
  | ✅ run 证据落盘 | \`run_id + trace_id + session_id\` 写入 \`/tmp/<task>_smoke_evidence.json\` 与 task_operation |
1546
1600
 
@@ -1714,6 +1768,8 @@ outputs: [] # 输出定义(可空)
1714
1768
  ## 严格约束
1715
1769
 
1716
1770
  - ✅ 只用 10 个被授予的工具(白名单强制)
1771
+ - ✅ **写 \`{{nodes.X.<字段>}}\` 前先核对访问契约表**(阶段 4.6):\`agent\` → \`.result\`;\`tool\` / \`pythonCall\` / \`skill\` → \`.output\`
1772
+ - ✅ **validate 必须迭代到 \`errorCount === 0\`**;每条 \`warning\` 要么修掉、要么在报告中写明保留理由
1717
1773
  - ✅ validate **必须**在 add 之前
1718
1774
  - ✅ **add / update 之后必须端到端冒烟跑一次**(阶段 5.5),并附 trace 证据;否则不算完成
1719
1775
  - ✅ tags 1-3 个,优先复用已有 tag
@@ -1725,6 +1781,9 @@ outputs: [] # 输出定义(可空)
1725
1781
  - ❌ 不要用 \`workflow update\` 绕过 validate / 冒烟
1726
1782
  - ❌ 不要用 \`bash\` 替代 workflow 节点完成业务逻辑(走捷径 = 交付空壳)
1727
1783
  - ❌ 不要凭"应该能跑"声称通过;**没有 trace / run 证据就不算跑过**
1784
+ - ❌ 不要在 \`tool\` / \`pythonCall\` / \`skill\` 节点上用 \`.result\`(引擎会静默解析成空字符串)
1785
+ - ❌ 不要对 \`tool\` 节点写深层属性路径(输出常为字符串,无法再取属性)
1786
+ - ❌ 不要在有 \`errors\` 未清零的情况下声称「语法检测 OK」
1728
1787
  - ❌ 不要自动 merge / push / tag
1729
1788
  - ❌ 不要硬编码大段 text(应引用 \`{{input.x}}\` 模板)`,
1730
1789
  "task-organizer": `
@@ -229,6 +229,29 @@ function buildTaskCard(opts) {
229
229
  const updateCount = opts.updateCount ?? 0;
230
230
  const spinner = opts.spinner ?? "";
231
231
  const elements = [];
232
+ const hasGroup = !!(opts.meta.perTaskChatId && opts.meta.perTaskChatId.length > 0);
233
+ const taskIdForButton = opts.meta.id !== undefined ? String(opts.meta.id) : "";
234
+ elements.push({
235
+ tag: "button",
236
+ text: {
237
+ tag: "plain_text",
238
+ content: hasGroup ? "解散该群" : "为这个任务建群"
239
+ },
240
+ type: hasGroup ? "danger" : "primary",
241
+ value: {
242
+ action: hasGroup ? "disband_group" : "create_group",
243
+ taskId: taskIdForButton
244
+ }
245
+ });
246
+ if (hasGroup) {
247
+ const link = buildChatAppLink(opts.meta.perTaskChatId);
248
+ const label = opts.meta.perTaskChatName ? `\uD83D\uDCE8 进入专属群:${sanitizeInlineText(opts.meta.perTaskChatName, 60)}` : "\uD83D\uDCE8 进入专属群";
249
+ elements.push({
250
+ tag: "markdown",
251
+ content: `[${label}](${link})`
252
+ });
253
+ elements.push({ tag: "hr" });
254
+ }
232
255
  const infoLines = renderTaskInfoLines(opts.meta);
233
256
  if (infoLines.length > 0) {
234
257
  elements.push({ tag: "markdown", content: infoLines.join(`
@@ -595,6 +618,25 @@ class LarkCardPatchNotifier {
595
618
  this.states.delete(opts.messageId);
596
619
  this.mutexes.delete(opts.messageId);
597
620
  }
621
+ updateMetaAndBuildCard(messageId, meta, opts = {}) {
622
+ const state = this.states.get(messageId);
623
+ if (!state)
624
+ return;
625
+ state.meta = mergeTaskMeta(state.meta, meta);
626
+ if (opts.clearPerTaskChat) {
627
+ delete state.meta.perTaskChatId;
628
+ delete state.meta.perTaskChatName;
629
+ }
630
+ return buildTaskCard({
631
+ status: "running",
632
+ meta: state.meta,
633
+ milestones: state.milestones,
634
+ processSteps: state.processSteps,
635
+ progress: state.progress,
636
+ updateCount: state.updateCount,
637
+ spinner: SPINNER_EMOJIS[state.spinnerIndex] ?? SPINNER_EMOJIS[0]
638
+ });
639
+ }
598
640
  trackedCardCount() {
599
641
  return this.states.size;
600
642
  }
@@ -1034,4 +1076,4 @@ function sanitize(text) {
1034
1076
  return `${trimmed.slice(0, 500)}…`;
1035
1077
  }
1036
1078
 
1037
- export { humanizeValue, formatDurationMs, LarkCardPatchNotifier };
1079
+ export { humanizeValue, buildTaskCard, formatDurationMs, LarkCardPatchNotifier };
@@ -0,0 +1,367 @@
1
+ import {
2
+ DAGManager,
3
+ init_dag_manager
4
+ } from "./roy-agent-core-8s9jd2qy.js";
5
+
6
+ // src/env/workflow/utils/node-output-contracts.ts
7
+ var VIRTUAL_OUTPUT_FIELD = "output";
8
+ var NODE_OUTPUT_CONTRACTS = {
9
+ tool: {
10
+ fields: [],
11
+ closed: true,
12
+ description: "a tool node's stored value is its result (usually a string)",
13
+ suggestedField: VIRTUAL_OUTPUT_FIELD
14
+ },
15
+ python_call: {
16
+ fields: [],
17
+ closed: true,
18
+ description: "a pythonCall node's stored value is its JSON-parsed stdout",
19
+ suggestedField: VIRTUAL_OUTPUT_FIELD
20
+ },
21
+ pythonCall: {
22
+ fields: [],
23
+ closed: true,
24
+ description: "a pythonCall node's stored value is its JSON-parsed stdout",
25
+ suggestedField: VIRTUAL_OUTPUT_FIELD
26
+ },
27
+ skill: {
28
+ fields: [],
29
+ closed: true,
30
+ description: "a skill node's stored value is the skill's return value",
31
+ suggestedField: VIRTUAL_OUTPUT_FIELD
32
+ },
33
+ condition: {
34
+ fields: ["success", "condition"],
35
+ closed: true,
36
+ description: "a condition node's stored value is { success, condition }",
37
+ suggestedField: VIRTUAL_OUTPUT_FIELD,
38
+ canonicalField: "condition"
39
+ },
40
+ merge: {
41
+ fields: ["strategy", "results", "count"],
42
+ closed: true,
43
+ description: "a merge node's stored value is { strategy, results, count }",
44
+ suggestedField: VIRTUAL_OUTPUT_FIELD,
45
+ canonicalField: "results"
46
+ },
47
+ agent: {
48
+ fields: ["result", "metadata", "workflowHistory"],
49
+ canonicalField: "result",
50
+ closed: false,
51
+ description: "an agent node's stored value is { result, metadata, workflowHistory, ...<json output fields> }",
52
+ suggestedField: "result"
53
+ },
54
+ workflow: {
55
+ fields: ["result"],
56
+ closed: false,
57
+ description: "a workflow node's stored value is the child workflow's top-level output",
58
+ suggestedField: VIRTUAL_OUTPUT_FIELD
59
+ },
60
+ ask_user: {
61
+ fields: [],
62
+ closed: true,
63
+ description: "an askUser node never produces output (it always pauses)"
64
+ },
65
+ askUser: {
66
+ fields: [],
67
+ closed: true,
68
+ description: "an askUser node never produces output (it always pauses)"
69
+ }
70
+ };
71
+ function getNodeOutputContract(nodeType) {
72
+ return NODE_OUTPUT_CONTRACTS[nodeType] ?? {
73
+ fields: [],
74
+ closed: false,
75
+ description: `unknown node type "${nodeType}" (output shape unknown)`
76
+ };
77
+ }
78
+ function getSuggestionFieldForResultMisuse(nodeType) {
79
+ const contract = getNodeOutputContract(nodeType);
80
+ return contract.canonicalField ?? contract.suggestedField ?? VIRTUAL_OUTPUT_FIELD;
81
+ }
82
+ function hasClosedOutputContract(nodeType) {
83
+ return getNodeOutputContract(nodeType).closed;
84
+ }
85
+ var RESULT_INVALID_NODE_TYPES = new Set([
86
+ "tool",
87
+ "python_call",
88
+ "pythonCall",
89
+ "skill",
90
+ "condition",
91
+ "merge",
92
+ "ask_user",
93
+ "askUser"
94
+ ]);
95
+ function getAgentDeclaredJsonFields(config) {
96
+ const output = config?.output;
97
+ const properties = output?.schema?.properties;
98
+ if (!properties || typeof properties !== "object")
99
+ return;
100
+ const keys = Object.keys(properties);
101
+ return keys.length > 0 ? keys : undefined;
102
+ }
103
+
104
+ // src/env/workflow/utils/template-contract-validator.ts
105
+ var TEMPLATE_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
106
+ var PURE_PATH_RE = /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+|\[-?\d+\])*$/;
107
+ var NON_NODE_SCOPES = new Set(["input", "inputs", "config", "env", "workflow", "vars", "secrets"]);
108
+ function isPurePath(body) {
109
+ return PURE_PATH_RE.test(body);
110
+ }
111
+ function walkStrings(value, visit) {
112
+ if (typeof value === "string") {
113
+ visit(value);
114
+ return;
115
+ }
116
+ if (Array.isArray(value)) {
117
+ for (const item of value)
118
+ walkStrings(item, visit);
119
+ return;
120
+ }
121
+ if (value && typeof value === "object") {
122
+ for (const v of Object.values(value))
123
+ walkStrings(v, visit);
124
+ }
125
+ }
126
+ function extractNodeRefs(config) {
127
+ const refs = [];
128
+ walkStrings(config, (s) => {
129
+ TEMPLATE_RE.lastIndex = 0;
130
+ let m;
131
+ while ((m = TEMPLATE_RE.exec(s)) !== null) {
132
+ const raw = m[0];
133
+ const body = m[1].split("|")[0].trim();
134
+ if (!body || /\s/.test(body) || !isPurePath(body))
135
+ continue;
136
+ if (body.startsWith("#") || body.startsWith("/"))
137
+ continue;
138
+ const parts = body.split(".");
139
+ let sourceNodeId;
140
+ let rest;
141
+ if (parts[0] === "nodes") {
142
+ if (parts.length < 2)
143
+ continue;
144
+ sourceNodeId = parts[1];
145
+ rest = parts.slice(2);
146
+ } else {
147
+ sourceNodeId = parts[0];
148
+ rest = parts.slice(1);
149
+ }
150
+ if (!sourceNodeId)
151
+ continue;
152
+ if (NON_NODE_SCOPES.has(sourceNodeId))
153
+ continue;
154
+ refs.push({ template: raw, sourceNodeId, rest });
155
+ }
156
+ });
157
+ return refs;
158
+ }
159
+ function baseSegment(seg) {
160
+ if (!seg)
161
+ return "";
162
+ return seg.replace(/\[-?\d+\]$/, "");
163
+ }
164
+ function buildGraph(nodes, edges) {
165
+ const byId = new Map;
166
+ const direct = new Map;
167
+ for (const n of nodes) {
168
+ if (!n || typeof n !== "object" || typeof n.id !== "string" || !n.id)
169
+ continue;
170
+ byId.set(n.id, n);
171
+ const deps = Array.isArray(n.depends_on) ? n.depends_on.filter((d) => typeof d === "string") : [];
172
+ direct.set(n.id, [...deps]);
173
+ }
174
+ for (const e of Array.isArray(edges) ? edges : []) {
175
+ if (!e || typeof e.from !== "string" || typeof e.to !== "string")
176
+ continue;
177
+ const to = direct.get(e.to);
178
+ if (!to)
179
+ continue;
180
+ if (!to.includes(e.from))
181
+ to.push(e.from);
182
+ }
183
+ const upstream = new Map;
184
+ for (const id of byId.keys()) {
185
+ const seen = new Set;
186
+ const stack = [...direct.get(id) ?? []];
187
+ while (stack.length > 0) {
188
+ const d = stack.pop();
189
+ if (seen.has(d))
190
+ continue;
191
+ seen.add(d);
192
+ for (const dd of direct.get(d) ?? [])
193
+ stack.push(dd);
194
+ }
195
+ upstream.set(id, seen);
196
+ }
197
+ return { byId, upstream };
198
+ }
199
+ function validateTemplateContracts(definition) {
200
+ if (!definition || typeof definition !== "object")
201
+ return [];
202
+ const def = definition;
203
+ if (!Array.isArray(def.nodes))
204
+ return [];
205
+ const nodes = def.nodes;
206
+ const { byId, upstream } = buildGraph(nodes, Array.isArray(def.edges) ? def.edges : []);
207
+ const findings = [];
208
+ for (const node of nodes) {
209
+ if (!node || typeof node !== "object" || typeof node.id !== "string" || !node.id)
210
+ continue;
211
+ const nodeId = node.id;
212
+ const refs = extractNodeRefs(node.config);
213
+ const upOfNode = upstream.get(nodeId) ?? new Set;
214
+ for (const ref of refs) {
215
+ const { template, sourceNodeId, rest } = ref;
216
+ const src = byId.get(sourceNodeId);
217
+ if (!src) {
218
+ findings.push({
219
+ severity: "error",
220
+ rule: "R1-node-not-found",
221
+ nodeId,
222
+ template,
223
+ sourceNodeId,
224
+ message: `Node "${nodeId}" references "${template}", but no node with id ` + `"${sourceNodeId}" exists in this workflow.`,
225
+ suggestion: `Remove the reference, or add a node with id "${sourceNodeId}".`
226
+ });
227
+ continue;
228
+ }
229
+ const srcType = src.type;
230
+ const contract = getNodeOutputContract(srcType);
231
+ const allowed = new Set([VIRTUAL_OUTPUT_FIELD, ...contract.fields]);
232
+ const first = baseSegment(rest[0]);
233
+ if (first === "result" && RESULT_INVALID_NODE_TYPES.has(srcType)) {
234
+ const suggestedField = getSuggestionFieldForResultMisuse(srcType);
235
+ const fixed = template.replace(new RegExp(`(nodes\\.${escapeRegExp(sourceNodeId)}|^\\{\\{${escapeRegExp(sourceNodeId)})\\.result`), `$1.${suggestedField}`);
236
+ findings.push({
237
+ severity: "error",
238
+ rule: "R2-result-on-non-agent",
239
+ nodeId,
240
+ template,
241
+ sourceNodeId,
242
+ sourceNodeType: srcType,
243
+ message: `Node "${nodeId}" references "${template}", but "${sourceNodeId}" is a ` + `"${srcType}" node — ${contract.description}. The ".result" field only ` + `exists on agent nodes; the value will silently resolve to an empty ` + `string at runtime. Use ".output" instead.`,
244
+ suggestion: fixed !== template ? fixed : undefined
245
+ });
246
+ continue;
247
+ }
248
+ if (first && contract.closed && !allowed.has(first)) {
249
+ findings.push({
250
+ severity: "warning",
251
+ rule: "R3-unknown-field-on-closed",
252
+ nodeId,
253
+ template,
254
+ sourceNodeId,
255
+ sourceNodeType: srcType,
256
+ message: `Node "${nodeId}" references "${template}"; "${sourceNodeId}" is a ` + `"${srcType}" node whose output is ${contract.description} — "${first}" ` + `is not one of [${[...allowed].join(", ")}]. It may resolve only if the ` + `node's output object happens to contain "${first}".`,
257
+ suggestion: contract.suggestedField ? template.replace(new RegExp(`\\.${escapeRegExp(first)}(?=\\.|\\[|\\}\\}|$)`), `.${contract.suggestedField}`) : undefined
258
+ });
259
+ }
260
+ if (srcType === "tool" && first === VIRTUAL_OUTPUT_FIELD && rest.length > 1) {
261
+ findings.push({
262
+ severity: "info",
263
+ rule: "R4-deep-path-into-tool",
264
+ nodeId,
265
+ template,
266
+ sourceNodeId,
267
+ sourceNodeType: srcType,
268
+ message: `Node "${nodeId}" references "${template}" — a deep path into a "tool" ` + `node. Tool outputs are frequently plain strings, in which case ` + `".${rest.slice(1).join(".")}" cannot be navigated. Verify the tool ` + `returns a structured object.`
269
+ });
270
+ }
271
+ if (srcType === "agent" && first) {
272
+ const declared = getAgentDeclaredJsonFields(src.config);
273
+ if (declared && first && !allowed.has(first) && !declared.includes(first)) {
274
+ findings.push({
275
+ severity: "warning",
276
+ rule: "R5-agent-schema-field",
277
+ nodeId,
278
+ template,
279
+ sourceNodeId,
280
+ sourceNodeType: srcType,
281
+ message: `Node "${nodeId}" references "${template}", but agent node ` + `"${sourceNodeId}" declares a JSON output schema with fields ` + `[${declared.join(", ")}]. "${first}" is not among them ` + `(nor the generic "result"/"output"/"metadata"/"workflowHistory").`
282
+ });
283
+ }
284
+ }
285
+ if (sourceNodeId !== nodeId && !upOfNode.has(sourceNodeId)) {
286
+ findings.push({
287
+ severity: "info",
288
+ rule: "R6-not-upstream",
289
+ nodeId,
290
+ template,
291
+ sourceNodeId,
292
+ sourceNodeType: srcType,
293
+ message: `Node "${nodeId}" references "${template}", but "${sourceNodeId}" is not ` + `in its upstream dependency closure. This is valid for loop back-edges ` + `(consuming the previous round's output) but otherwise may resolve to an ` + `empty value on the first execution.`
294
+ });
295
+ }
296
+ }
297
+ }
298
+ return findings;
299
+ }
300
+ function escapeRegExp(s) {
301
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
302
+ }
303
+ function getTemplateContractErrors(definition) {
304
+ return validateTemplateContracts(definition).filter((f) => f.severity === "error");
305
+ }
306
+ function getTemplateContractWarnings(definition) {
307
+ return validateTemplateContracts(definition).filter((f) => f.severity === "warning");
308
+ }
309
+ function formatTemplateContractFindings(findings) {
310
+ return findings.map((f) => {
311
+ const head = `[template-contract:${f.rule}] Node "${f.nodeId}": ${f.message}`;
312
+ return f.suggestion ? `${head} — Did you mean "${f.suggestion}"?` : head;
313
+ });
314
+ }
315
+
316
+ // src/env/workflow/utils/validate-workflow-definition.ts
317
+ init_dag_manager();
318
+ function validateWorkflowDefinition(definition, options = {}) {
319
+ if (!definition || typeof definition !== "object") {
320
+ return {
321
+ valid: false,
322
+ errors: ["Workflow definition must be an object"]
323
+ };
324
+ }
325
+ const def = definition;
326
+ if (!Array.isArray(def.nodes)) {
327
+ return {
328
+ valid: false,
329
+ errors: ["Workflow definition must have a 'nodes' array"]
330
+ };
331
+ }
332
+ let structural;
333
+ try {
334
+ const dag = new DAGManager(definition, { allowCycles: false });
335
+ structural = dag.validate();
336
+ } catch (error) {
337
+ structural = {
338
+ valid: false,
339
+ errors: [
340
+ `Validation error: ${error instanceof Error ? error.message : String(error)}`
341
+ ]
342
+ };
343
+ }
344
+ let findings = [];
345
+ if (structural.valid && !options.skipTemplateContracts) {
346
+ try {
347
+ findings = validateTemplateContracts(definition);
348
+ } catch {
349
+ findings = [];
350
+ }
351
+ }
352
+ const contractErrors = findings.filter((f) => f.severity === "error");
353
+ const contractWarnings = findings.filter((f) => f.severity === "warning");
354
+ const contractInfos = findings.filter((f) => f.severity === "info");
355
+ const errors = [...structural.errors, ...formatTemplateContractFindings(contractErrors)];
356
+ const warnings = formatTemplateContractFindings(contractWarnings);
357
+ const infos = formatTemplateContractFindings(contractInfos);
358
+ return {
359
+ valid: structural.valid && contractErrors.length === 0,
360
+ errors,
361
+ ...warnings.length > 0 ? { warnings } : {},
362
+ ...infos.length > 0 ? { infos } : {},
363
+ ...findings.length > 0 ? { findings } : {}
364
+ };
365
+ }
366
+
367
+ export { VIRTUAL_OUTPUT_FIELD, NODE_OUTPUT_CONTRACTS, getNodeOutputContract, hasClosedOutputContract, RESULT_INVALID_NODE_TYPES, getAgentDeclaredJsonFields, validateTemplateContracts, getTemplateContractErrors, getTemplateContractWarnings, formatTemplateContractFindings, validateWorkflowDefinition };
@@ -10,7 +10,7 @@ import {
10
10
  } from "./roy-agent-core-1pg0fepg.js";
11
11
  import {
12
12
  builtInPrompts
13
- } from "./roy-agent-core-cg9f4zch.js";
13
+ } from "./roy-agent-core-gdsqp6j8.js";
14
14
  import {
15
15
  DEFAULT_SUBAGENT_PROMPT
16
16
  } from "./roy-agent-core-rx16h2m1.js";