feat: 为 gitbundle 装配 required skills (#138)
* Add gitbundle required skills validation * fix: 限定 required skill blocked result 覆盖 --------- Co-authored-by: AgentRun Codex <agentrun@example.invalid> Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
@@ -91,13 +91,14 @@ HWLAB canary 创建 run 时应使用以下字段口径:
|
|||||||
| `resourceBundleRef.kind` | 必须是 `gitbundle`。 |
|
| `resourceBundleRef.kind` | 必须是 `gitbundle`。 |
|
||||||
| `resourceBundleRef.bundles[]` | 用于承接 HWLAB 固定工具和 skill 子树,默认 `tools -> tools`、`skills -> .agents/skills`;旧字段不得再发送。 |
|
| `resourceBundleRef.bundles[]` | 用于承接 HWLAB 固定工具和 skill 子树,默认 `tools -> tools`、`skills -> .agents/skills`;旧字段不得再发送。 |
|
||||||
| `resourceBundleRef.promptRefs[]` | 用于承接 HWLAB 稳定初始 prompt,例如 `hwlab-v02-runtime`;必须来自同一 materialized gitbundle checkout,`inject=thread-start`,新 thread 首轮注入,resume 不注入。 |
|
| `resourceBundleRef.promptRefs[]` | 用于承接 HWLAB 稳定初始 prompt,例如 `hwlab-v02-runtime`;必须来自同一 materialized gitbundle checkout,`inject=thread-start`,新 thread 首轮注入,resume 不注入。 |
|
||||||
|
| `resourceBundleRef.requiredSkills[]` | 用于声明本次运行必须存在的 gitbundle skill,例如 `{ "name": "dad-dev" }`;runner 只校验 `.agents/skills/<name>/SKILL.md`,不得接受 inline manifest、host path、Secret 或旧 `skillRefs`。 |
|
||||||
| `executionPolicy` | sandbox、network、timeout、secretScope 必须显式,不得由 HWLAB 扩大 AgentRun Secret 范围。 |
|
| `executionPolicy` | sandbox、network、timeout、secretScope 必须显式,不得由 HWLAB 扩大 AgentRun Secret 范围。 |
|
||||||
| `executionPolicy.secretScope.toolCredentials[]` | 需要 UniDesk SSH passthrough 时必须声明 `tool=unidesk-ssh`、`purpose=ssh-passthrough`、SecretRef `agentrun-v01-tool-unidesk-ssh`、projection env `UNIDESK_SSH_CLIENT_TOKEN`;不得把 token 放入 command payload 或 runner-job transientEnv。 |
|
| `executionPolicy.secretScope.toolCredentials[]` | 需要 UniDesk SSH passthrough 时必须声明 `tool=unidesk-ssh`、`purpose=ssh-passthrough`、SecretRef `agentrun-v01-tool-unidesk-ssh`、projection env `UNIDESK_SSH_CLIENT_TOKEN`;不得把 token 放入 command payload 或 runner-job transientEnv。 |
|
||||||
| `traceSink` | 可指向 HWLAB trace adapter;为 `null` 时 HWLAB 仍可通过 AgentRun events 轮询。 |
|
| `traceSink` | 可指向 HWLAB trace adapter;为 `null` 时 HWLAB 仍可通过 AgentRun events 轮询。 |
|
||||||
|
|
||||||
`tenantId` / `projectId` 是 AgentRun manager 的 policy 边界,不是 HWLAB Workbench project。HWLAB adapter 可以把业务 project/workspace 写入 `metadata.hwlabProjectId`、`metadata.hwlabWorkspaceId` 或 `workspaceRef`,但不得覆盖 AgentRun `projectId=pikasTech/HWLAB`;否则 manager 必须按 `tenant-policy-denied` 拒绝。`providerProfile` 是 HWLAB 入口字段,进入 AgentRun 后必须映射为 `backendProfile`;同一个 HWLAB session 的后续 turn 应继承 session provider profile,不能被 stale workspace provider 覆盖。
|
`tenantId` / `projectId` 是 AgentRun manager 的 policy 边界,不是 HWLAB Workbench project。HWLAB adapter 可以把业务 project/workspace 写入 `metadata.hwlabProjectId`、`metadata.hwlabWorkspaceId` 或 `workspaceRef`,但不得覆盖 AgentRun `projectId=pikasTech/HWLAB`;否则 manager 必须按 `tenant-policy-denied` 拒绝。`providerProfile` 是 HWLAB 入口字段,进入 AgentRun 后必须映射为 `backendProfile`;同一个 HWLAB session 的后续 turn 应继承 session provider profile,不能被 stale workspace provider 覆盖。
|
||||||
|
|
||||||
Command 第一阶段要求 `type=turn` 和 `type=steer`。`turn` 保存用户原始 prompt、conversation metadata、profile 选择和 HWLAB trace correlation;稳定业务 prompt、skill 清单和工具入口不写入 command payload,而是通过 `ResourceBundleRef.kind="gitbundle"` 的 `bundles[]` 与 `promptRefs` 装配。`steer` 保存运行中引导文本,并由 runner 在同 run active turn 期间转发到 backend。业务 cancel 仍走 run/command cancel API,不用 `steer` 伪装。不得把 cookie、session token、provider credential、HWPOD internal token、Secret value、历史消息或大段 skill manifest 写入 payload。
|
Command 第一阶段要求 `type=turn` 和 `type=steer`。`turn` 保存用户原始 prompt、conversation metadata、profile 选择和 HWLAB trace correlation;稳定业务 prompt、skill 清单和工具入口不写入 command payload,而是通过 `ResourceBundleRef.kind="gitbundle"` 的 `bundles[]`、`promptRefs` 与 `requiredSkills` 装配。`steer` 保存运行中引导文本,并由 runner 在同 run active turn 期间转发到 backend。业务 cancel 仍走 run/command cancel API,不用 `steer` 伪装。不得把 cookie、session token、provider credential、HWPOD internal token、Secret value、历史消息或大段 skill manifest 写入 payload。
|
||||||
|
|
||||||
## 需要补齐的能力
|
## 需要补齐的能力
|
||||||
|
|
||||||
@@ -164,6 +165,9 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
],
|
],
|
||||||
"promptRefs": [
|
"promptRefs": [
|
||||||
{ "name": "hwlab-v02-runtime", "path": "internal/agent/prompts/hwlab-v02-runtime.md", "inject": "thread-start", "required": true }
|
{ "name": "hwlab-v02-runtime", "path": "internal/agent/prompts/hwlab-v02-runtime.md", "inject": "thread-start", "required": true }
|
||||||
|
],
|
||||||
|
"requiredSkills": [
|
||||||
|
{ "name": "dad-dev" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +175,7 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
|
|
||||||
这些 prompt 文件只写稳定规则,例如 HWLAB Cloud Workbench Code Agent 身份、HWPOD 四要素(target device、workspace、debug probe、io probe)、D601-F103-V2/Keil/build/download/UART 的标准 `hwpod` 路径、禁止旧 Device Pod/profile/device-pod-cli fallback、禁止 Cloud Web 业务 API 替代 runner 内 HWPOD CLI、禁止 session-token fallback、禁止长路径 wrapper,以及历史上下文只走 Codex stdio 原生 `thread/resume`。它们不写用户本轮任务、不写会话历史、不写 Secret 或一次性 issue 过程。
|
这些 prompt 文件只写稳定规则,例如 HWLAB Cloud Workbench Code Agent 身份、HWPOD 四要素(target device、workspace、debug probe、io probe)、D601-F103-V2/Keil/build/download/UART 的标准 `hwpod` 路径、禁止旧 Device Pod/profile/device-pod-cli fallback、禁止 Cloud Web 业务 API 替代 runner 内 HWPOD CLI、禁止 session-token fallback、禁止长路径 wrapper,以及历史上下文只走 Codex stdio 原生 `thread/resume`。它们不写用户本轮任务、不写会话历史、不写 Secret 或一次性 issue 过程。
|
||||||
|
|
||||||
首轮新 thread 必须自动注入这些 prompt 和 skill facts,使 Web 简短 prompt 也能识别 HWLAB 标准能力。后续 turn/resume 不重复注入;若 resume 失败,按 AgentRun Codex stdio 规则失败,不拼接历史 prompt 模拟继续。
|
首轮新 thread 必须自动注入这些 prompt 和 skill facts,使 Web 简短 prompt 也能识别 HWLAB 标准能力。`requiredSkills` 声明的 skill 缺失时必须在 resource assembly 阶段 blocked,failureKind 为 `required-skill-unavailable`,result/event 带 missing/available/hash/bytes 摘要。后续 turn/resume 不重复注入;若 resume 失败,按 AgentRun Codex stdio 规则失败,不拼接历史 prompt 模拟继续。
|
||||||
|
|
||||||
## 分阶段增强计划
|
## 分阶段增强计划
|
||||||
|
|
||||||
@@ -181,7 +185,7 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
| 2 | trace/result 元语 | 标准 event 子集、terminal result envelope、bounded output metadata | HWLAB 可由 events 稳定生成 result/trace;partial 不误报 completed。 |
|
| 2 | trace/result 元语 | 标准 event 子集、terminal result envelope、bounded output metadata | HWLAB 可由 events 稳定生成 result/trace;partial 不误报 completed。 |
|
||||||
| 3 | cancel 闭环 | durable cancel API、runner cancel poll、backend interrupt/process group stop | pending/running/terminal 后 cancel 均幂等且可见。 |
|
| 3 | cancel 闭环 | durable cancel API、runner cancel poll、backend interrupt/process group stop | pending/running/terminal 后 cancel 均幂等且可见。 |
|
||||||
| 4 | ResourceBundleRef materialization | Git-only checkout、workspace 前缀、requested ref/commit、actual commit/tree 摘要、failureKind | 从 repo/ref 解析 actual commit;不依赖 cloud-api 或 CI/CD rollout 的 artifact revision;不覆盖 Secret/session/runtime home。 |
|
| 4 | ResourceBundleRef materialization | Git-only checkout、workspace 前缀、requested ref/commit、actual commit/tree 摘要、failureKind | 从 repo/ref 解析 actual commit;不依赖 cloud-api 或 CI/CD rollout 的 artifact revision;不覆盖 Secret/session/runtime home。 |
|
||||||
| 5 | Resource prompt/skill assembly | `promptRefs` thread-start 注入、gitbundle skillDirs 发现、hash/bytes 可见 | 简短 HWLAB prompt 能看到业务 instruction 和 gitbundle skills;resume 不重复注入;旧字段直接拒绝。 |
|
| 5 | Resource prompt/skill assembly | `promptRefs` thread-start 注入、gitbundle skillDirs 发现、requiredSkills 校验、hash/bytes 可见 | 简短 HWLAB prompt 能看到业务 instruction 和 gitbundle skills;required skill 缺失 blocked;resume 不重复注入;旧字段直接拒绝。 |
|
||||||
| 6 | SessionRef 持久化与 runner 多 turn | session record/store、thread resume、runner command loop、TTL/GC、profile 隔离 | 同一 conversation 连续两轮复用同一 run/runner Job;第二轮不重新 materialize bundle、不重复注入 initial prompt;不同 profile 不污染。 |
|
| 6 | SessionRef 持久化与 runner 多 turn | session record/store、thread resume、runner command loop、TTL/GC、profile 隔离 | 同一 conversation 连续两轮复用同一 run/runner Job;第二轮不重新 materialize bundle、不重复注入 initial prompt;不同 profile 不污染。 |
|
||||||
| 7 | HWLAB v0.2 canary | HWLAB dispatcher adapter、traceId 映射、result/trace 转换 | 普通自然语言最短 turn 真实 completed 且 reply 非空;HWPOD 仍由 HWLAB 授权。 |
|
| 7 | HWLAB v0.2 canary | HWLAB dispatcher adapter、traceId 映射、result/trace 转换 | 普通自然语言最短 turn 真实 completed 且 reply 非空;HWPOD 仍由 HWLAB 授权。 |
|
||||||
|
|
||||||
@@ -213,7 +217,7 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
|
|
||||||
### T7 HWLAB prompt/skill 装配
|
### T7 HWLAB prompt/skill 装配
|
||||||
|
|
||||||
阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),然后用 HWLAB canary `ResourceBundleRef.kind="gitbundle"` 指定 `bundles[]` 和 `promptRefs`。首轮 Web/CLI 简短 prompt 只写“编译 D601-F103-V2”或等价自然语言,确认 Codex turn 能看到 HWLAB runtime prompt、`.agents/skills` 中的 `hwpod-cli`/`hwpod-ctl` skill facts 和 `tools/hwpod` 命令;第二轮 continuation 使用同一 thread resume,确认 `initialPromptInjected=false`,没有手工拼接历史;旧字段请求必须 schema-invalid。
|
阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),然后用 HWLAB canary `ResourceBundleRef.kind="gitbundle"` 指定 `bundles[]`、`promptRefs` 和 `requiredSkills`。首轮 Web/CLI 简短 prompt 只写“编译 D601-F103-V2”或等价自然语言,确认 Codex turn 能看到 HWLAB runtime prompt、`.agents/skills` 中的 `hwpod-cli`/`hwpod-ctl`/`dad-dev` skill facts 和 `tools/hwpod` 命令;另跑一个只装配 tools 但声明 required dad-dev 的 payload,确认装配阶段 `required-skill-unavailable` blocked;第二轮 continuation 使用同一 thread resume,确认 `initialPromptInjected=false`,没有手工拼接历史;旧字段请求必须 schema-invalid。
|
||||||
|
|
||||||
### T8 DS 短 prompt 真实验收
|
### T8 DS 短 prompt 真实验收
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
2. “你当前 HWLAB 初始规则里,D601-F103-V2 应该走哪个标准入口?请只回答入口和禁止路径。”确认回复能说出 `hwpod`、`hwpod-cli`/`hwpod-ctl`、assembled runtime env、禁止旧 Device Pod/profile/device-pod-cli fallback、禁止 session-token fallback 和禁止长路径 wrapper。
|
2. “你当前 HWLAB 初始规则里,D601-F103-V2 应该走哪个标准入口?请只回答入口和禁止路径。”确认回复能说出 `hwpod`、`hwpod-cli`/`hwpod-ctl`、assembled runtime env、禁止旧 Device Pod/profile/device-pod-cli fallback、禁止 session-token fallback 和禁止长路径 wrapper。
|
||||||
3. “编译 D601-F103-V2。”确认短 prompt 能按注入规则触发 `hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node` 正向链路,而不是要求用户补充长 prompt。
|
3. “编译 D601-F103-V2。”确认短 prompt 能按注入规则触发 `hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node` 正向链路,而不是要求用户补充长 prompt。
|
||||||
|
|
||||||
上述三条必须来自真实 DS/DeepSeek profile 的 Codex stdio `thread/start`/`turn/start` 和后续 `thread/resume` 事件;trace/result 必须显示 `initialPromptInjected=true` 的首轮、gitbundle skillDirs 摘要、真实 provider profile、terminal status,以及 continuation 时 `initialPromptInjected=false`。如果回复只列出 Codex 默认系统 skill、不能识别 HWLAB 初始规则或需要用户长 prompt 才能触发 `hwpod`,验收失败。
|
上述三条必须来自真实 DS/DeepSeek profile 的 Codex stdio `thread/start`/`turn/start` 和后续 `thread/resume` 事件;trace/result 必须显示 `initialPromptInjected=true` 的首轮、gitbundle skillDirs/requiredSkills 摘要、真实 provider profile、terminal status,以及 continuation 时 `initialPromptInjected=false`。如果回复只列出 Codex 默认系统 skill、不能识别 HWLAB 初始规则、缺失 required skill 没有 blocked,或需要用户长 prompt 才能触发 `hwpod`,验收失败。
|
||||||
|
|
||||||
## 实现状态
|
## 实现状态
|
||||||
|
|
||||||
@@ -234,6 +238,6 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须收敛为 `gitbundle
|
|||||||
| cancel | 已实现最小闭环 | 已提供 run/command cancel API;pending cancel 会阻止新 runner Job,running runner 通过轮询触发 backend abort,终态写入 event、command state 和 run status。 |
|
| cancel | 已实现最小闭环 | 已提供 run/command cancel API;pending cancel 会阻止新 runner Job,running runner 通过轮询触发 backend abort,终态写入 event、command state 和 run status。 |
|
||||||
| SessionRef | 已实现最小持久化 | run 可携带 `sessionRef`,manager 保存 session/thread,runner 会按 threadId resume,result envelope 暴露脱敏 session 摘要;TTL/GC 仍按后续运维策略细化。 |
|
| SessionRef | 已实现最小持久化 | run 可携带 `sessionRef`,manager 保存 session/thread,runner 会按 threadId resume,result envelope 暴露脱敏 session 摘要;TTL/GC 仍按后续运维策略细化。 |
|
||||||
| SessionRef | v0.1.1 已实现/已通过 HWLAB v0.2 原入口复测 | 在「metadata-only 最小持久化」基础上把 session 真实持久化:每个 session 绑 RWO PVC(`agentrun-v01-session-<sessionId>`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,codex app-server 自己落盘;HWLAB 原入口已验证 runner pod 删除后同 session/thread/PVC 可以恢复,仍禁止 fake 续接。 |
|
| SessionRef | v0.1.1 已实现/已通过 HWLAB v0.2 原入口复测 | 在「metadata-only 最小持久化」基础上把 session 真实持久化:每个 session 绑 RWO PVC(`agentrun-v01-session-<sessionId>`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,codex app-server 自己落盘;HWLAB 原入口已验证 runner pod 删除后同 session/thread/PVC 可以恢复,仍禁止 fake 续接。 |
|
||||||
| ResourceBundleRef | 已实现 `kind="gitbundle"` materialization/promptRefs/tools/skillDirs 装配 | run 可携带 `repoUrl + ref/branch + bundles[]`,runner checkout 到 `AGENTRUN_WORKSPACE_ROOT` 下的隔离目录并记录 requested ref/commit、actual commit/tree/workspace/bundles 摘要;`tools/` PATH、`promptRefs` thread-start 注入和 `.agents/skills` 目录发现已实现;上传文件、inline seed 和对象存储不进入 v0.1。 |
|
| ResourceBundleRef | 已实现 `kind="gitbundle"` materialization/promptRefs/tools/skillDirs/requiredSkills 装配 | run 可携带 `repoUrl + ref/branch + bundles[]`,runner checkout 到 `AGENTRUN_WORKSPACE_ROOT` 下的隔离目录并记录 requested ref/commit、actual commit/tree/workspace/bundles 摘要;`tools/` PATH、`promptRefs` thread-start 注入、`.agents/skills` 目录发现和 required skill 校验已实现;上传文件、inline seed、inline skill manifest 和对象存储不进入 v0.1。 |
|
||||||
| 同 run/runner 多 turn | 已实现最小闭环 | runner Job 在 idle timeout 内持续 poll 同一 run 的后续 command;普通 turn completed 不终结 run,bundle 只 materialize 一次,command result 按 commandId 独立聚合。 |
|
| 同 run/runner 多 turn | 已实现最小闭环 | runner Job 在 idle timeout 内持续 poll 同一 run 的后续 command;普通 turn completed 不终结 run,bundle 只 materialize 一次,command result 按 commandId 独立聚合。 |
|
||||||
| HWLAB v0.2 canary | 已实现/已通过 HWLAB v0.2 原入口复测 | HWLAB dispatcher adapter 已调 AgentRun 手动调度 API,并能转换 result/trace;MiniMax-M3 显式 session、provider profile 继承和 runner pod 删除后的同 session resume 已通过原入口 CLI 复测。 |
|
| HWLAB v0.2 canary | 已实现/已通过 HWLAB v0.2 原入口复测 | HWLAB dispatcher adapter 已调 AgentRun 手动调度 API,并能转换 result/trace;MiniMax-M3 显式 session、provider profile 继承和 runner pod 删除后的同 session resume 已通过原入口 CLI 复测。 |
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ Session 命令负责输出、trace 和会话控制:
|
|||||||
|
|
||||||
不得新增 `queue output`、`queue trace` 或 `queue session/*` 这类子路径代理。`queue list/show/commander` 默认输出低噪声 summary,最多打印 task/attempt/session ids、状态、统计、`sessionPath` 和下一步 `sessions ...` 命令;完整 payload/resource bundle/metadata 只能通过显式 `--full|--raw` 展开。Queue mutation 命令带 `--dry-run` 时必须只返回 `mutation=false` 的计划,不得写 Queue、Core run/command 或 runner job。
|
不得新增 `queue output`、`queue trace` 或 `queue session/*` 这类子路径代理。`queue list/show/commander` 默认输出低噪声 summary,最多打印 task/attempt/session ids、状态、统计、`sessionPath` 和下一步 `sessions ...` 命令;完整 payload/resource bundle/metadata 只能通过显式 `--full|--raw` 展开。Queue mutation 命令带 `--dry-run` 时必须只返回 `mutation=false` 的计划,不得写 Queue、Core run/command 或 runner job。
|
||||||
|
|
||||||
|
Queue task 的 `resourceBundleRef` 在 dispatch 时原样进入 Core run。若其中声明 `requiredSkills`,Queue 只展示声明和终态摘要,不能自行判定可用;runner 必须在 gitbundle materialization 后、backend 启动前校验 `.agents/skills/<name>/SKILL.md`,缺失时以 `required-skill-unavailable` 写入 command/run result 和 events。
|
||||||
|
|
||||||
## 数据模型方向
|
## 数据模型方向
|
||||||
|
|
||||||
Queue 首版新增或扩展的稳定表方向:
|
Queue 首版新增或扩展的稳定表方向:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
| `BackendImageRef` | `image` | digest-pinned backend/runner 镜像。 | API KEY、profile config、用户代码、session 文件。 |
|
| `BackendImageRef` | `image` | digest-pinned backend/runner 镜像。 | API KEY、profile config、用户代码、session 文件。 |
|
||||||
| `ProfileRef` | `profile`、`secretRef` | provider profile 和 API KEY/配置 SecretRef。 | backend 镜像、session、repo 文件、GitHub/业务工具 credential。 |
|
| `ProfileRef` | `profile`、`secretRef` | provider profile 和 API KEY/配置 SecretRef。 | backend 镜像、session、repo 文件、GitHub/业务工具 credential。 |
|
||||||
| `SessionRef` | `sessionId` 或 `null` | backend 会话文件持久化引用;P0 可以为 `null`。 | API KEY、完整 `CODEX_HOME`、Git workspace。 |
|
| `SessionRef` | `sessionId` 或 `null` | backend 会话文件持久化引用;P0 可以为 `null`。 | API KEY、完整 `CODEX_HOME`、Git workspace。 |
|
||||||
| `ResourceBundleRef` | `kind="gitbundle"`、`repoUrl`、`bundles[]`、可选 `ref` / `commitId` / `promptRefs` | 初始代码/文件输入、工具目录、skill 目录和稳定初始 prompt;P0 固定 Git-only gitbundle,默认从 repo/ref 解析实际 commit。 | 上传文件、对象存储 artifact、inline env、Secret value、会话历史、旧 inline seed。 |
|
| `ResourceBundleRef` | `kind="gitbundle"`、`repoUrl`、`bundles[]`、可选 `ref` / `commitId` / `promptRefs` / `requiredSkills` | 初始代码/文件输入、工具目录、skill 目录、required skill 校验和稳定初始 prompt;P0 固定 Git-only gitbundle,默认从 repo/ref 解析实际 commit。 | 上传文件、对象存储 artifact、inline env、Secret value、会话历史、旧 inline seed、inline skill manifest。 |
|
||||||
|
|
||||||
P0 最小 JSON 形态:
|
P0 最小 JSON 形态:
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@ P0 最小 JSON 形态:
|
|||||||
{ "name": "tools", "subpath": "tools", "target_path": "tools" },
|
{ "name": "tools", "subpath": "tools", "target_path": "tools" },
|
||||||
{ "name": "skills", "subpath": "skills", "target_path": ".agents/skills" }
|
{ "name": "skills", "subpath": "skills", "target_path": ".agents/skills" }
|
||||||
],
|
],
|
||||||
"promptRefs": []
|
"promptRefs": [],
|
||||||
|
"requiredSkills": [{ "name": "dad-dev" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -164,7 +165,8 @@ HWLAB Workbench 的 project/workspace 不属于 RuntimeAssembly 四要素,也
|
|||||||
- `subpath` 必须留在 checkout 内,`target_path` 必须留在 runner workspace 内;runner 按 `subpath -> target_path` 复制文件或目录。
|
- `subpath` 必须留在 checkout 内,`target_path` 必须留在 runner workspace 内;runner 按 `subpath -> target_path` 复制文件或目录。
|
||||||
- `credentialRef` 只用于拉取私有 Git repo,不等同于 backend API KEY。
|
- `credentialRef` 只用于拉取私有 Git repo,不等同于 backend API KEY。
|
||||||
- 不支持上传文件、对象存储 artifact、任意 ConfigMap 文件袋、inline seed 或旧字段;旧 `toolAliases`、`skillRefs`、`workspaceFiles`、`subdir`、`sparsePaths` 输入必须直接 schema-invalid。
|
- 不支持上传文件、对象存储 artifact、任意 ConfigMap 文件袋、inline seed 或旧字段;旧 `toolAliases`、`skillRefs`、`workspaceFiles`、`subdir`、`sparsePaths` 输入必须直接 schema-invalid。
|
||||||
- 面向 HWLAB 手动调度 canary,默认 bundle 把 repo 的 `tools/` 复制到 workspace `tools/`,把 `skills/` 复制到 workspace `.agents/skills/`;event/result 必须记录 repo、requested ref/commit、实际 materialized commit、checkout tree、bundle 列表、workspace 摘要、tools 和 skillDirs 摘要。
|
- `requiredSkills[]` 只接受 `{ "name": "<skill-name>" }` 这类声明式元数据;runner 只能在 gitbundle 已复制的 `.agents/skills/<name>/SKILL.md` 中校验,不接受正文、host path、SecretRef、旧 `skillRefs` 或模型默认 skill fallback。
|
||||||
|
- 面向 HWLAB 手动调度 canary,默认 bundle 把 repo 的 `tools/` 复制到 workspace `tools/`,把 `skills/` 复制到 workspace `.agents/skills/`;event/result 必须记录 repo、requested ref/commit、实际 materialized commit、checkout tree、bundle 列表、workspace 摘要、tools、skillDirs 和 requiredSkills 摘要。
|
||||||
|
|
||||||
#### tools 目录
|
#### tools 目录
|
||||||
|
|
||||||
@@ -198,7 +200,9 @@ AgentRun 自身仓库必须提供 `tools/tran` 与 `tools/trans`,用于承接
|
|||||||
|
|
||||||
#### skill 目录
|
#### skill 目录
|
||||||
|
|
||||||
skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.md`。runner 发现直接子目录中的 `SKILL.md` 后,把 `.agents/skills` 暴露给 backend,并在 initial prompt 中加入有界 skill facts。多文件 skill 的 `references/`、`scripts/` 和后续新增文件跟随目录一起复制;event/result 只输出 skill name、manifest path 摘要、hash、bytes 和 summary,不输出大段 manifest 正文。runner 不读取镜像默认 skill、host path、ConfigMap、用户长 prompt 或旧 `skillRefs`。
|
skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.md`。runner 发现直接子目录中的 `SKILL.md` 后,把 `.agents/skills` 暴露给 backend,并在 initial prompt 中加入有界 skill facts。多文件 skill 的 `references/`、`scripts/` 和后续新增文件跟随目录一起复制;event/result 只输出 skill name、manifest path 摘要、hash、bytes、summary 和来源 bundle 摘要,不输出大段 manifest 正文。
|
||||||
|
|
||||||
|
若调用方声明 `requiredSkills[]`,runner 必须在 backend 启动前逐项校验 `.agents/skills/<name>/SKILL.md` 已由 gitbundle 物化。缺失时 run/command blocked,failureKind 为 `required-skill-unavailable`,blocker details 至少包含 required、missing 和 available 摘要;不得读取镜像默认 skill、host path、ConfigMap、用户长 prompt 或旧 `skillRefs` 作为替代。
|
||||||
|
|
||||||
#### 初始 prompt 与 session 边界
|
#### 初始 prompt 与 session 边界
|
||||||
|
|
||||||
@@ -212,8 +216,8 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
|||||||
4. Runner materialize profile Secret 到 writable runtime home。
|
4. Runner materialize profile Secret 到 writable runtime home。
|
||||||
5. Runner materialize tool credential 到该 run 允许的 env/file projection;未实现的 tool scope 必须显式 failed/blocked,不能静默跳过后让 agent 自己猜凭据。
|
5. Runner materialize tool credential 到该 run 允许的 env/file projection;未实现的 tool scope 必须显式 failed/blocked,不能静默跳过后让 agent 自己猜凭据。
|
||||||
6. Runner materialize `kind="gitbundle"` resource bundle 到 workspace;P0 未实现时必须显式 blocked,不能猜测 host path。
|
6. Runner materialize `kind="gitbundle"` resource bundle 到 workspace;P0 未实现时必须显式 blocked,不能猜测 host path。
|
||||||
7. Runner 按 `bundles[]` 复制目录或文件,准备 workspace `tools/`、发现 `.agents/skills`,读取并校验 `promptRefs`,写入有界 assembly event。
|
7. Runner 按 `bundles[]` 复制目录或文件,准备 workspace `tools/`、发现 `.agents/skills`,校验 `requiredSkills`,读取并校验 `promptRefs`,写入有界 assembly event。
|
||||||
8. Runner 启动 backend,并在 event 中记录 image digest、profile、SecretRef 名称/key、tool credential scope、sessionRef、repoUrl、requested ref/commit、materialized commit、bundles、promptRefs、tools 和 skillDirs 摘要。
|
8. Runner 启动 backend,并在 event 中记录 image digest、profile、SecretRef 名称/key、tool credential scope、sessionRef、repoUrl、requested ref/commit、materialized commit、bundles、promptRefs、requiredSkills、tools 和 skillDirs 摘要。
|
||||||
|
|
||||||
任何一个要素缺失或不合法,都必须按该要素失败;不得静默 fallback。
|
任何一个要素缺失或不合法,都必须按该要素失败;不得静默 fallback。
|
||||||
|
|
||||||
@@ -256,7 +260,7 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
|||||||
- run payload 不携带文件正文、env dump、Secret value 或大型 artifact。
|
- run payload 不携带文件正文、env dump、Secret value 或大型 artifact。
|
||||||
- 若提供 `bundles[]`,必须能看到每个 `subpath -> target_path` 的复制摘要;旧字段输入必须 schema-invalid。
|
- 若提供 `bundles[]`,必须能看到每个 `subpath -> target_path` 的复制摘要;旧字段输入必须 schema-invalid。
|
||||||
- 若提供 `promptRefs`,必须能看到每个 prompt 的 `name/path/sha256/bytes/inject`,新 thread 首轮 `initialPromptInjected=true`,resume turn `initialPromptInjected=false`。
|
- 若提供 `promptRefs`,必须能看到每个 prompt 的 `name/path/sha256/bytes/inject`,新 thread 首轮 `initialPromptInjected=true`,resume turn `initialPromptInjected=false`。
|
||||||
- 若 bundle 复制了 `.agents/skills`,必须能看到 skillDirs 聚合摘要、skill 名称和 manifest hash;不能显示模型默认 skill 列表当作业务 skill。
|
- 若 bundle 复制了 `.agents/skills`,必须能看到 skillDirs 聚合摘要、skill 名称、manifest hash/bytes 和来源 bundle;若提供 `requiredSkills`,必须看到成功路径的 requiredSkills hash/bytes,以及缺失路径的 `required-skill-unavailable` blocker。不能显示模型默认 skill 列表当作业务 skill。
|
||||||
|
|
||||||
### A5 综合验收
|
### A5 综合验收
|
||||||
|
|
||||||
@@ -266,7 +270,7 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
|||||||
2. 用哪一个 profile 和 SecretRef。
|
2. 用哪一个 profile 和 SecretRef。
|
||||||
3. 是否使用 session;若不用,必须明确为 `null`/deferred。
|
3. 是否使用 session;若不用,必须明确为 `null`/deferred。
|
||||||
4. 使用哪一个 Git repo/ref,以及最终 materialized 到哪一个 full commit;若 P0 尚未 materialize,必须明确为 deferred,不能隐式使用 host path。
|
4. 使用哪一个 Git repo/ref,以及最终 materialized 到哪一个 full commit;若 P0 尚未 materialize,必须明确为 deferred,不能隐式使用 host path。
|
||||||
5. 是否装配 gitbundle bundles、workspace tools、初始 prompt 和 skillDirs;若提供,必须能回答 name/path/hash/inject/required 和是否注入,不能只依赖模型默认 prompt 或默认 skill registry。
|
5. 是否装配 gitbundle bundles、workspace tools、初始 prompt、skillDirs 和 requiredSkills;若提供,必须能回答 name/path/hash/bytes/inject/required 和是否注入,不能只依赖模型默认 prompt 或默认 skill registry。
|
||||||
6. 是否装配 tool credential;若需要 GitHub PR 能力,必须能回答 tool、purpose、SecretRef 和 projection kind,不能只在运行时 shell 中偶然存在 token。
|
6. 是否装配 tool credential;若需要 GitHub PR 能力,必须能回答 tool、purpose、SecretRef 和 projection kind,不能只在运行时 shell 中偶然存在 token。
|
||||||
|
|
||||||
## 实现状态
|
## 实现状态
|
||||||
@@ -277,6 +281,6 @@ skill 只来自 gitbundle 复制进 workspace 的 `.agents/skills/<name>/SKILL.m
|
|||||||
| `ProfileRef` | 已实现/待 dsflash-go 真实主闭环 | `codex`、`deepseek` 与 `minimax-m3` 已通过 SecretRef、writable runtime home 和真实 stdio turn 验证;MiniMax-M3 已通过 HWLAB 显式 session 原入口复测。`dsflash-go` 已补齐 SecretRef/model catalog 装配、自测试和 legacy key 归一,仍需完成真实 runtime 与 HWLAB 原入口复测;后续只允许作为 profile/config/SecretRef/model catalog 选择,不新增直连 backend。 |
|
| `ProfileRef` | 已实现/待 dsflash-go 真实主闭环 | `codex`、`deepseek` 与 `minimax-m3` 已通过 SecretRef、writable runtime home 和真实 stdio turn 验证;MiniMax-M3 已通过 HWLAB 显式 session 原入口复测。`dsflash-go` 已补齐 SecretRef/model catalog 装配、自测试和 legacy key 归一,仍需完成真实 runtime 与 HWLAB 原入口复测;后续只允许作为 profile/config/SecretRef/model catalog 选择,不新增直连 backend。 |
|
||||||
| `SessionRef` | 已实现最小持久化 | manager 持久化 `sessionId/conversationId/threadId`,run 创建会解析既有 session,runner 按 threadId resume;session 不保存 credential 文件,TTL/GC 后续细化。 |
|
| `SessionRef` | 已实现最小持久化 | manager 持久化 `sessionId/conversationId/threadId`,run 创建会解析既有 session,runner 按 threadId resume;session 不保存 credential 文件,TTL/GC 后续细化。 |
|
||||||
| `SessionRef` | v0.1.1 已实现/已通过 HWLAB v0.2 原入口复测 | manager 持久化 `sessionId/conversationId/threadId` + 每个 session 绑 RWO PVC(`agentrun-v01-session-<sessionId>`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,codex app-server 自己落盘;runner pod 删除后 replacement runner 仍复用同一 SessionRef/PVC/thread,禁止 copy/restore、replacement threadId 和 fake resume。 |
|
| `SessionRef` | v0.1.1 已实现/已通过 HWLAB v0.2 原入口复测 | manager 持久化 `sessionId/conversationId/threadId` + 每个 session 绑 RWO PVC(`agentrun-v01-session-<sessionId>`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/<codex_rollout_subdir>`,codex app-server 自己落盘;runner pod 删除后 replacement runner 仍复用同一 SessionRef/PVC/thread,禁止 copy/restore、replacement threadId 和 fake resume。 |
|
||||||
| `ResourceBundleRef` | 已实现 `kind="gitbundle"` materialization/promptRefs/tools/skillDirs 装配 | `repoUrl + ref/materialized commit + bundles[]` 已进入 run schema 和 runner checkout,workspace 受 `AGENTRUN_WORKSPACE_ROOT` 限制,event/result 记录 requested ref/commit、actual commit、tree/workspace/bundles 摘要;`tools/` PATH、`promptRefs` thread-start 注入和 `.agents/skills` 目录发现已实现。 |
|
| `ResourceBundleRef` | 已实现 `kind="gitbundle"` materialization/promptRefs/tools/skillDirs/requiredSkills 装配 | `repoUrl + ref/materialized commit + bundles[]` 已进入 run schema 和 runner checkout,workspace 受 `AGENTRUN_WORKSPACE_ROOT` 限制,event/result 记录 requested ref/commit、actual commit、tree/workspace/bundles 摘要;`tools/` PATH、`promptRefs` thread-start 注入、`.agents/skills` 目录发现和 required skill 校验已实现。 |
|
||||||
| `toolCredentials` | 已实现最小 env projection | GitHub PR 和 UniDesk SSH passthrough 等 agent shell/tool 授权通过装配 SPEC 的 SecretRef 进入 runner;v0.1 支持 `tool=github` 与 `tool=unidesk-ssh`、`projection.kind=env`,runner Job 使用 `valueFrom.secretKeyRef` 注入,不用 `transientEnv` 绕过。 |
|
| `toolCredentials` | 已实现最小 env projection | GitHub PR 和 UniDesk SSH passthrough 等 agent shell/tool 授权通过装配 SPEC 的 SecretRef 进入 runner;v0.1 支持 `tool=github` 与 `tool=unidesk-ssh`、`projection.kind=env`,runner Job 使用 `valueFrom.secretKeyRef` 注入,不用 `transientEnv` 绕过。 |
|
||||||
| `transientEnv` | 已实现 per-job SecretRef 投影 | Queue dispatch 和 runner-job API 支持短期 runtime env;正式 Kubernetes Job 先创建本次 Job 专属 Secret,再用 `valueFrom.secretKeyRef` 注入 runner env,response/event/trace 只显示 env names、Secret metadata 和 `valuesPrinted=false`。 |
|
| `transientEnv` | 已实现 per-job SecretRef 投影 | Queue dispatch 和 runner-job API 支持短期 runtime env;正式 Kubernetes Job 先创建本次 Job 专属 Secret,再用 `valueFrom.secretKeyRef` 注入 runner env,response/event/trace 只显示 env names、Secret metadata 和 `valuesPrinted=false`。 |
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
- Postgres adapter:migration、事务、run/command/event round-trip、重启后可查询。
|
- Postgres adapter:migration、事务、run/command/event round-trip、重启后可查询。
|
||||||
- Secret 分发:SecretRef schema、missing secret failure、redaction。
|
- Secret 分发:SecretRef schema、missing secret failure、redaction。
|
||||||
- AgentRun Queue:task schema、attempt 状态机、summary/stats/read cursor、Queue 与 Session 引用边界、旧 MiniMax/OpenCode 直连入口废弃和 redaction。
|
- AgentRun Queue:task schema、attempt 状态机、summary/stats/read cursor、Queue 与 Session 引用边界、旧 MiniMax/OpenCode 直连入口废弃和 redaction。
|
||||||
- HWLAB v0.2 基线承接:可以用 fake backend/临时 manager 做组件自测试,覆盖 event contract、result completed 防误判、bounded output、runner job status、SessionRef profile 隔离、ResourceBundleRef 失败分类、`promptRefs`/gitbundle skillDirs 装配和 backend preflight redaction;这些自测试不能替代真实 `agentrun-v01` CLI 交互验收。
|
- HWLAB v0.2 基线承接:可以用 fake backend/临时 manager 做组件自测试,覆盖 event contract、result completed 防误判、bounded output、runner job status、SessionRef profile 隔离、ResourceBundleRef 失败分类、`promptRefs`/gitbundle skillDirs/requiredSkills 装配和 backend preflight redaction;这些自测试不能替代真实 `agentrun-v01` CLI 交互验收。
|
||||||
|
|
||||||
自测试应使用 Bun + TypeScript 运行,Codex 相关自测试可以使用 fake app-server JSON-RPC client 模拟 `initialize`、`thread/start`、`thread/resume`、`turn/start`、assistant 输出、协议错误、timeout 和 transport close。
|
自测试应使用 Bun + TypeScript 运行,Codex 相关自测试可以使用 fake app-server JSON-RPC client 模拟 `initialize`、`thread/start`、`thread/resume`、`turn/start`、assistant 输出、协议错误、timeout 和 transport close。
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
6. manager 可查询 command state、append-only events、terminal_status 和 redacted logPath/job identity。
|
6. manager 可查询 command state、append-only events、terminal_status 和 redacted logPath/job identity。
|
||||||
7. 重启 `agentrun-mgr` 后,run、command、events 和 terminal_status 仍可从 Postgres 查询。
|
7. 重启 `agentrun-mgr` 后,run、command、events 和 terminal_status 仍可从 Postgres 查询。
|
||||||
8. 日志、event、CLI 输出和 health 中没有 provider credential、DSN password、token 或 URL credential 明文。
|
8. 日志、event、CLI 输出和 health 中没有 provider credential、DSN password、token 或 URL credential 明文。
|
||||||
9. 若变更涉及 RuntimeAssembly,必须能追溯 `BackendImageRef`、`ProfileRef`、`SessionRef` 和 `ResourceBundleRef` 的装配状态;未提供 session/resource 时必须显式为 `null`,提供时必须能查到 session/thread 和 Git commit/tree/workspace/bundles 摘要,不能由 runner 隐式猜测。若提供 `promptRefs` 或 gitbundle skills,必须能查到 name/path/hash/bytes/injected 摘要;required prompt 缺失必须 blocked,不能 fallback 到模型默认 prompt 或默认 skill registry。
|
9. 若变更涉及 RuntimeAssembly,必须能追溯 `BackendImageRef`、`ProfileRef`、`SessionRef` 和 `ResourceBundleRef` 的装配状态;未提供 session/resource 时必须显式为 `null`,提供时必须能查到 session/thread 和 Git commit/tree/workspace/bundles 摘要,不能由 runner 隐式猜测。若提供 `promptRefs`、gitbundle skills 或 `requiredSkills`,必须能查到 name/path/hash/bytes/injected 摘要;required prompt 或 required skill 缺失必须 blocked,不能 fallback 到模型默认 prompt 或默认 skill registry。
|
||||||
|
|
||||||
### CLI 交互联调标准
|
### CLI 交互联调标准
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ CLI 与 RESTful API 可以复用同一个真实 run 做联调。若两者观察
|
|||||||
| cancel | 对 pending/running/terminal 分别调用 cancel | cancel 幂等,pending 不再启动 runner,running 收敛为 cancelled 或既有 terminal,events/result 可见。 |
|
| cancel | 对 pending/running/terminal 分别调用 cancel | cancel 幂等,pending 不再启动 runner,running 收敛为 cancelled 或既有 terminal,events/result 可见。 |
|
||||||
| SessionRef | 连续两轮使用同一 sessionRef 或 conversation/session/thread 摘要 | 第二轮可 resume backend thread;session 不包含 credential 文件或完整 CODEX_HOME。 |
|
| SessionRef | 连续两轮使用同一 sessionRef 或 conversation/session/thread 摘要 | 第二轮可 resume backend thread;session 不包含 credential 文件或完整 CODEX_HOME。 |
|
||||||
| ResourceBundleRef | 使用 `repoUrl + ref/branch + bundles[]` 启动 runner | runner checkout 到允许 workspace,event/result 能回答 repo、requested ref/commit、actual commit、workspace 摘要;不使用 host path、cloud-api artifact revision 或 CI/CD rollout 状态作为 bundle 内容来源。 |
|
| ResourceBundleRef | 使用 `repoUrl + ref/branch + bundles[]` 启动 runner | runner checkout 到允许 workspace,event/result 能回答 repo、requested ref/commit、actual commit、workspace 摘要;不使用 host path、cloud-api artifact revision 或 CI/CD rollout 状态作为 bundle 内容来源。 |
|
||||||
| Resource prompt/skill assembly | 使用同一 `ResourceBundleRef.kind="gitbundle"` 指定 `bundles[]` 和 `promptRefs` | 新 thread 首轮注入 initial prompt 和 gitbundle skill facts;resume 不重复注入;required prompt 缺失 blocked;不使用用户长 prompt、旧硬编码 prompt、镜像 `/app/skills` 或默认 Codex skill registry 替代。 |
|
| Resource prompt/skill assembly | 使用同一 `ResourceBundleRef.kind="gitbundle"` 指定 `bundles[]`、`promptRefs` 和 `requiredSkills` | 新 thread 首轮注入 initial prompt 和 gitbundle skill facts;resume 不重复注入;required prompt 缺失 blocked;tools-only payload 声明 required skill 时必须在装配阶段 `required-skill-unavailable` blocked;不使用用户长 prompt、旧硬编码 prompt、镜像 `/app/skills` 或默认 Codex skill registry 替代。 |
|
||||||
| DS 短 prompt 探测 | 通过正式 CLI/Web 等价入口向真实 `backendProfile=deepseek` 发送短 prompt | “可见 skill”回复包含 gitbundle `.agents/skills` 中的业务 skill 而不只是 Codex 默认系统 skill;“当前 HWLAB 初始规则”能回答 hwpod/HWPOD 四要素/禁止路径;“编译 D601-F103-V2”能触发 `hwpod -> hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node` 正向链路。 |
|
| DS 短 prompt 探测 | 通过正式 CLI/Web 等价入口向真实 `backendProfile=deepseek` 发送短 prompt | “可见 skill”回复包含 gitbundle `.agents/skills` 中的业务 skill 而不只是 Codex 默认系统 skill;“当前 HWLAB 初始规则”能回答 hwpod/HWPOD 四要素/禁止路径;“编译 D601-F103-V2”能触发 `hwpod -> hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node` 正向链路。 |
|
||||||
| ProfileRef/SecretRef | 分别验证 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile | 只使用当前 profile SecretRef;`dsflash-go` 必须投影 `model-catalog.json`;缺失时 `secret-unavailable`,不 fallback,不泄露 Secret 值。 |
|
| ProfileRef/SecretRef | 分别验证 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile | 只使用当前 profile SecretRef;`dsflash-go` 必须投影 `model-catalog.json`;缺失时 `secret-unavailable`,不 fallback,不泄露 Secret 值。 |
|
||||||
| bounded output | 触发工具/命令输出摘要 | result/event 只含摘要、字节数、截断标记和必要引用,不把大 stdout/stderr 塞入单个 JSON 响应。 |
|
| bounded output | 触发工具/命令输出摘要 | result/event 只含摘要、字节数、截断标记和必要引用,不把大 stdout/stderr 塞入单个 JSON 响应。 |
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type FailureKind =
|
|||||||
| "secret-unavailable"
|
| "secret-unavailable"
|
||||||
| "prompt-unavailable"
|
| "prompt-unavailable"
|
||||||
| "prompt-too-large"
|
| "prompt-too-large"
|
||||||
|
| "required-skill-unavailable"
|
||||||
| "skill-unavailable"
|
| "skill-unavailable"
|
||||||
| "runner-lease-conflict"
|
| "runner-lease-conflict"
|
||||||
| "backend-failed"
|
| "backend-failed"
|
||||||
@@ -81,6 +82,9 @@ export interface ResourceBundleRef extends JsonRecord {
|
|||||||
inject?: "thread-start";
|
inject?: "thread-start";
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
requiredSkills?: Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
submodules?: false;
|
submodules?: false;
|
||||||
lfs?: false;
|
lfs?: false;
|
||||||
credentialRef?: SecretRef;
|
credentialRef?: SecretRef;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
|
|||||||
rejectLegacyResourceBundleFields(record);
|
rejectLegacyResourceBundleFields(record);
|
||||||
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, ...(commitId ? { commitId } : {}), ...(ref ? { ref } : {}), bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId, ref) };
|
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, ...(commitId ? { commitId } : {}), ...(ref ? { ref } : {}), bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId, ref) };
|
||||||
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
|
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
|
||||||
|
if (record.requiredSkills !== undefined) result.requiredSkills = validateResourceRequiredSkills(record.requiredSkills);
|
||||||
if (record.submodules !== undefined && record.submodules !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.submodules can only be false in v0.1", { httpStatus: 400 });
|
if (record.submodules !== undefined && record.submodules !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.submodules can only be false in v0.1", { httpStatus: 400 });
|
||||||
if (record.lfs !== undefined && record.lfs !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.lfs can only be false in v0.1", { httpStatus: 400 });
|
if (record.lfs !== undefined && record.lfs !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.lfs can only be false in v0.1", { httpStatus: 400 });
|
||||||
if (record.submodules === false) result.submodules = false;
|
if (record.submodules === false) result.submodules = false;
|
||||||
@@ -160,6 +161,22 @@ function validateResourcePromptRefs(value: unknown): NonNullable<ResourceBundleR
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateResourceRequiredSkills(value: unknown): NonNullable<ResourceBundleRef["requiredSkills"]> {
|
||||||
|
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.requiredSkills must be an array", { httpStatus: 400 });
|
||||||
|
if (value.length > 32) throw new AgentRunError("schema-invalid", "resourceBundleRef.requiredSkills must contain at most 32 entries", { httpStatus: 400 });
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return value.map((entry, index) => {
|
||||||
|
const record = asRecord(entry, `resourceBundleRef.requiredSkills[${index}]`);
|
||||||
|
const allowedKeys = new Set(["name"]);
|
||||||
|
const extraKeys = Object.keys(record).filter((key) => !allowedKeys.has(key));
|
||||||
|
if (extraKeys.length > 0) throw new AgentRunError("schema-invalid", `resourceBundleRef.requiredSkills[${index}] only supports name`, { httpStatus: 400, details: { rejectedKeys: extraKeys.sort(), valuesPrinted: false } });
|
||||||
|
const name = validateResourceName(requiredString(record, "name"), `resourceBundleRef.requiredSkills[${index}].name`);
|
||||||
|
if (seen.has(name)) throw new AgentRunError("schema-invalid", `resourceBundleRef.requiredSkills name ${name} is duplicated`, { httpStatus: 400 });
|
||||||
|
seen.add(name);
|
||||||
|
return { name };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function validateResourceName(name: string, fieldName: string): string {
|
function validateResourceName(name: string, fieldName: string): string {
|
||||||
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new AgentRunError("schema-invalid", `${fieldName} must be a lowercase resource name`, { httpStatus: 400 });
|
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new AgentRunError("schema-invalid", `${fieldName} must be a lowercase resource name`, { httpStatus: 400 });
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
+63
-8
@@ -1,5 +1,5 @@
|
|||||||
import type { AgentRunStore } from "./store.js";
|
import type { AgentRunStore } from "./store.js";
|
||||||
import type { CommandRecord, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
|
import type { CommandRecord, FailureKind, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
|
||||||
import { boundedTextSummary, outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js";
|
import { boundedTextSummary, outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js";
|
||||||
|
|
||||||
const maxToolCallSummaryItems = 40;
|
const maxToolCallSummaryItems = 40;
|
||||||
@@ -35,12 +35,14 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
|
|||||||
const latestJob = jobs.at(-1) ?? null;
|
const latestJob = jobs.at(-1) ?? null;
|
||||||
const commandTerminal = command ? terminalFromCommand(command) : null;
|
const commandTerminal = command ? terminalFromCommand(command) : null;
|
||||||
const terminalEventStatus = terminalFromEvents(scopedEvents);
|
const terminalEventStatus = terminalFromEvents(scopedEvents);
|
||||||
const terminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus;
|
const preliminaryTerminal = commandTerminal ?? terminalEventStatus ?? run.terminalStatus;
|
||||||
const terminalSource = commandTerminal ? "command-record" : terminalEventStatus ? "terminal_status-event" : run.terminalStatus ? "run-record" : "none";
|
const failureKind = resultFailureKind(run, command, scopedEvents, preliminaryTerminal);
|
||||||
const failureKind = resultFailureKind(run, command, scopedEvents, terminal);
|
const terminal = resultTerminal(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
|
||||||
|
const terminalSource = resultTerminalSource(commandTerminal, terminalEventStatus, run.terminalStatus, failureKind);
|
||||||
const failureMessage = resultFailureMessage(run, command, scopedEvents, terminal);
|
const failureMessage = resultFailureMessage(run, command, scopedEvents, terminal);
|
||||||
|
const failureDetails = resultFailureDetails(scopedEvents, terminal);
|
||||||
const reply = assistantReply(scopedEvents);
|
const reply = assistantReply(scopedEvents);
|
||||||
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage } : null;
|
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: failureMessage, details: failureDetails } : null;
|
||||||
const liveness = livenessSnapshot(run, command, events, scopedEvents, terminal);
|
const liveness = livenessSnapshot(run, command, events, scopedEvents, terminal);
|
||||||
const steerDelivery = command?.type === "steer" ? steerDeliverySummary(events, command.id) : null;
|
const steerDelivery = command?.type === "steer" ? steerDeliverySummary(events, command.id) : null;
|
||||||
return {
|
return {
|
||||||
@@ -72,6 +74,7 @@ export async function buildRunResult(store: AgentRunStore, runId: string, comman
|
|||||||
finalAssistantOutputTruncated: reply.outputTruncated,
|
finalAssistantOutputTruncated: reply.outputTruncated,
|
||||||
failureKind,
|
failureKind,
|
||||||
failureMessage,
|
failureMessage,
|
||||||
|
failureDetails,
|
||||||
blocker,
|
blocker,
|
||||||
liveness,
|
liveness,
|
||||||
...(steerDelivery ? { steerDelivery } : {}),
|
...(steerDelivery ? { steerDelivery } : {}),
|
||||||
@@ -292,25 +295,64 @@ function terminalFromCommand(command: CommandRecord): TerminalStatus | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resultTerminal(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): TerminalStatus | null {
|
||||||
|
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "blocked";
|
||||||
|
return commandTerminal ?? terminalEventStatus ?? runTerminalStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultTerminalSource(commandTerminal: TerminalStatus | null, terminalEventStatus: TerminalStatus | null, runTerminalStatus: TerminalStatus | null, failureKind: FailureKind | null): string {
|
||||||
|
if (commandTerminal === "failed" && terminalEventStatus === "blocked" && failureKind === "required-skill-unavailable") return "terminal_status-event";
|
||||||
|
if (commandTerminal) return "command-record";
|
||||||
|
if (terminalEventStatus) return "terminal_status-event";
|
||||||
|
if (runTerminalStatus) return "run-record";
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
function eventsForCommand(events: RunEvent[], commandId: string): RunEvent[] {
|
function eventsForCommand(events: RunEvent[], commandId: string): RunEvent[] {
|
||||||
const scoped = events.filter((event) => event.payload.commandId === commandId || typeof event.payload.commandId !== "string");
|
const scoped = events.filter((event) => event.payload.commandId === commandId || typeof event.payload.commandId !== "string");
|
||||||
return scoped.length > 0 ? scoped : events;
|
return scoped.length > 0 ? scoped : events;
|
||||||
}
|
}
|
||||||
|
|
||||||
function failureKindFromEvents(events: RunEvent[]): string | null {
|
function failureKindFromEvents(events: RunEvent[]): FailureKind | null {
|
||||||
for (const event of [...events].reverse()) {
|
for (const event of [...events].reverse()) {
|
||||||
const value = event.payload.failureKind;
|
const value = event.payload.failureKind;
|
||||||
if (typeof value === "string") return value;
|
if (isFailureKind(value)) return value;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): string | null {
|
function resultFailureKind(run: RunRecord, command: CommandRecord | null, events: RunEvent[], terminal: TerminalStatus | null): FailureKind | null {
|
||||||
if (terminal === "completed") return null;
|
if (terminal === "completed") return null;
|
||||||
if (command) return failureKindFromEvents(events);
|
if (command) return failureKindFromEvents(events);
|
||||||
return run.failureKind ?? failureKindFromEvents(events);
|
return run.failureKind ?? failureKindFromEvents(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFailureKind(value: unknown): value is FailureKind {
|
||||||
|
return typeof value === "string" && [
|
||||||
|
"cancelled",
|
||||||
|
"tenant-policy-denied",
|
||||||
|
"secret-unavailable",
|
||||||
|
"prompt-unavailable",
|
||||||
|
"prompt-too-large",
|
||||||
|
"required-skill-unavailable",
|
||||||
|
"skill-unavailable",
|
||||||
|
"runner-lease-conflict",
|
||||||
|
"backend-failed",
|
||||||
|
"backend-timeout",
|
||||||
|
"backend-response-invalid",
|
||||||
|
"thread-resume-failed",
|
||||||
|
"provider-auth-failed",
|
||||||
|
"provider-invalid-tool-call",
|
||||||
|
"provider-compact-unsupported",
|
||||||
|
"provider-rate-limited",
|
||||||
|
"provider-unavailable",
|
||||||
|
"provider-stream-disconnected",
|
||||||
|
"provider-refused-retry-recovered",
|
||||||
|
"infra-failed",
|
||||||
|
"schema-invalid",
|
||||||
|
].includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
function messageFromEvents(events: RunEvent[]): string | null {
|
function messageFromEvents(events: RunEvent[]): string | null {
|
||||||
for (const event of [...events].reverse()) {
|
for (const event of [...events].reverse()) {
|
||||||
const value = event.payload.message;
|
const value = event.payload.message;
|
||||||
@@ -325,6 +367,18 @@ function resultFailureMessage(run: RunRecord, command: CommandRecord | null, eve
|
|||||||
return run.failureMessage ?? messageFromEvents(events);
|
return run.failureMessage ?? messageFromEvents(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detailsFromEvents(events: RunEvent[]): JsonRecord | null {
|
||||||
|
for (const event of [...events].reverse()) {
|
||||||
|
const value = event.payload.details;
|
||||||
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value as JsonRecord;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultFailureDetails(events: RunEvent[], terminal: TerminalStatus | null): JsonRecord | null {
|
||||||
|
return terminal === "completed" ? null : detailsFromEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
function assistantReply(events: RunEvent[]): AssistantReplySummary {
|
function assistantReply(events: RunEvent[]): AssistantReplySummary {
|
||||||
const assistantEvents = events.filter((event) => event.type === "assistant_message");
|
const assistantEvents = events.filter((event) => event.type === "assistant_message");
|
||||||
const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0);
|
const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0);
|
||||||
@@ -474,6 +528,7 @@ function resourceBundleSummary(run: RunRecord, events: RunEvent[]): JsonRecord |
|
|||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
},
|
},
|
||||||
promptRefs: run.resourceBundleRef.promptRefs ? { count: run.resourceBundleRef.promptRefs.length, names: run.resourceBundleRef.promptRefs.map((item) => item.name), required: run.resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
promptRefs: run.resourceBundleRef.promptRefs ? { count: run.resourceBundleRef.promptRefs.length, names: run.resourceBundleRef.promptRefs.map((item) => item.name), required: run.resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
||||||
|
requiredSkills: run.resourceBundleRef.requiredSkills ? { count: run.resourceBundleRef.requiredSkills.length, names: run.resourceBundleRef.requiredSkills.map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], valuesPrinted: false },
|
||||||
materialized: materialized as JsonValue,
|
materialized: materialized as JsonValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,6 +761,7 @@ export function summarizeResourceBundleRef(resourceBundleRef: RunRecord["resourc
|
|||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
},
|
},
|
||||||
promptRefs: resourceBundleRef.promptRefs ? { count: resourceBundleRef.promptRefs.length, names: resourceBundleRef.promptRefs.map((item) => item.name), required: resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
promptRefs: resourceBundleRef.promptRefs ? { count: resourceBundleRef.promptRefs.length, names: resourceBundleRef.promptRefs.map((item) => item.name), required: resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
||||||
|
requiredSkills: resourceBundleRef.requiredSkills ? { count: resourceBundleRef.requiredSkills.length, names: resourceBundleRef.requiredSkills.map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], valuesPrinted: false },
|
||||||
submodules: resourceBundleRef.submodules ?? false,
|
submodules: resourceBundleRef.submodules ?? false,
|
||||||
lfs: resourceBundleRef.lfs ?? false,
|
lfs: resourceBundleRef.lfs ?? false,
|
||||||
credentialRef: resourceBundleRef.credentialRef ? { name: resourceBundleRef.credentialRef.name, namespace: resourceBundleRef.credentialRef.namespace ?? null, keys: resourceBundleRef.credentialRef.keys ?? [], valuesPrinted: false } : null,
|
credentialRef: resourceBundleRef.credentialRef ? { name: resourceBundleRef.credentialRef.name, namespace: resourceBundleRef.credentialRef.namespace ?? null, keys: resourceBundleRef.credentialRef.keys ?? [], valuesPrinted: false } : null,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function failureKindFromError(error: unknown): FailureKind {
|
|||||||
|
|
||||||
export function terminalStatusForFailure(failureKind: FailureKind): TerminalStatus {
|
export function terminalStatusForFailure(failureKind: FailureKind): TerminalStatus {
|
||||||
if (failureKind === "cancelled") return "cancelled";
|
if (failureKind === "cancelled") return "cancelled";
|
||||||
if (failureKind === "secret-unavailable" || failureKind === "tenant-policy-denied" || failureKind === "schema-invalid" || failureKind === "prompt-unavailable" || failureKind === "prompt-too-large" || failureKind === "skill-unavailable") return "blocked";
|
if (failureKind === "secret-unavailable" || failureKind === "tenant-policy-denied" || failureKind === "schema-invalid" || failureKind === "prompt-unavailable" || failureKind === "prompt-too-large" || failureKind === "required-skill-unavailable" || failureKind === "skill-unavailable") return "blocked";
|
||||||
return "failed";
|
return "failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface MaterializedSkillRef {
|
|||||||
manifestBytes: number;
|
manifestBytes: number;
|
||||||
manifestSha256: string;
|
manifestSha256: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
|
sourceBundle: JsonRecord | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitCheckout {
|
interface GitCheckout {
|
||||||
@@ -91,7 +92,8 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
|||||||
const defaultCheckout = await checkoutFor(defaultSource);
|
const defaultCheckout = await checkoutFor(defaultSource);
|
||||||
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
||||||
const tools = await prepareGitBundleTools(workspacePath, env);
|
const tools = await prepareGitBundleTools(workspacePath, env);
|
||||||
const skills = await discoverGitBundleSkills(workspacePath);
|
const skills = await discoverGitBundleSkills(workspacePath, materializedBundles);
|
||||||
|
const requiredSkills = materializeRequiredSkills(resourceBundleRef.requiredSkills ?? [], skills.items);
|
||||||
const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []);
|
const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []);
|
||||||
const initialPrompt = assembleInitialPrompt(prompts.items, skills.items);
|
const initialPrompt = assembleInitialPrompt(prompts.items, skills.items);
|
||||||
return {
|
return {
|
||||||
@@ -116,6 +118,7 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
|||||||
},
|
},
|
||||||
tools: tools.event,
|
tools: tools.event,
|
||||||
skillDirs: skills.event,
|
skillDirs: skills.event,
|
||||||
|
requiredSkills: requiredSkills.event,
|
||||||
promptRefs: prompts.event,
|
promptRefs: prompts.event,
|
||||||
initialPrompt: initialPrompt?.summary ?? { available: false, bytes: 0, sha256: null, promptRefCount: prompts.items.length, skillCount: skills.items.length, valuesPrinted: false },
|
initialPrompt: initialPrompt?.summary ?? { available: false, bytes: 0, sha256: null, promptRefCount: prompts.items.length, skillCount: skills.items.length, valuesPrinted: false },
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
@@ -215,7 +218,7 @@ async function prepareGitBundleTools(workspacePath: string, env: NodeJS.ProcessE
|
|||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
const items: JsonRecord[] = [];
|
const items: JsonRecord[] = [];
|
||||||
if (installedBinPath) await mkdir(installedBinPath, { recursive: true });
|
if (installedBinPath) await mkdir(installedBinPath, { recursive: true });
|
||||||
for (const entry of entries) {
|
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
||||||
if (!entry.isFile()) continue;
|
if (!entry.isFile()) continue;
|
||||||
const filePath = path.join(sourceBinPath, entry.name);
|
const filePath = path.join(sourceBinPath, entry.name);
|
||||||
const text = await readFile(filePath, "utf8");
|
const text = await readFile(filePath, "utf8");
|
||||||
@@ -283,7 +286,7 @@ async function materializePromptRefs(checkoutPath: string, refs: NonNullable<Res
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discoverGitBundleSkills(workspacePath: string): Promise<{ items: MaterializedSkillRef[]; skillsDir?: string; event: JsonRecord }> {
|
async function discoverGitBundleSkills(workspacePath: string, bundles: MaterializedGitBundle[]): Promise<{ items: MaterializedSkillRef[]; skillsDir?: string; event: JsonRecord }> {
|
||||||
const skillsDir = path.join(workspacePath, ".agents", "skills");
|
const skillsDir = path.join(workspacePath, ".agents", "skills");
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
@@ -294,22 +297,24 @@ async function discoverGitBundleSkills(workspacePath: string): Promise<{ items:
|
|||||||
}
|
}
|
||||||
const items: MaterializedSkillRef[] = [];
|
const items: MaterializedSkillRef[] = [];
|
||||||
const eventItems: JsonRecord[] = [];
|
const eventItems: JsonRecord[] = [];
|
||||||
for (const entry of entries) {
|
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
||||||
if (!entry.isDirectory()) continue;
|
if (!entry.isDirectory()) continue;
|
||||||
const aggregateAs = entry.name;
|
const aggregateAs = entry.name;
|
||||||
|
const relativeManifestPath = `.agents/skills/${aggregateAs}/SKILL.md`;
|
||||||
const manifestPath = path.join(skillsDir, aggregateAs, "SKILL.md");
|
const manifestPath = path.join(skillsDir, aggregateAs, "SKILL.md");
|
||||||
|
const sourceBundle = skillSourceBundle(relativeManifestPath, bundles);
|
||||||
let manifestText: string;
|
let manifestText: string;
|
||||||
try {
|
try {
|
||||||
manifestText = await readFile(manifestPath, "utf8");
|
manifestText = await readFile(manifestPath, "utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
eventItems.push({ name: aggregateAs, path: `.agents/skills/${aggregateAs}/SKILL.md`, required: true, aggregateAs, status: "missing", error: fileErrorSummary(error), valuesPrinted: false });
|
eventItems.push({ name: aggregateAs, path: relativeManifestPath, required: true, aggregateAs, status: "missing", error: fileErrorSummary(error), sourceBundle, valuesPrinted: false });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bytes = Buffer.byteLength(manifestText, "utf8");
|
const bytes = Buffer.byteLength(manifestText, "utf8");
|
||||||
const sha = sha256Text(manifestText);
|
const sha = sha256Text(manifestText);
|
||||||
const summary = skillSummary(manifestText);
|
const summary = skillSummary(manifestText);
|
||||||
items.push({ name: aggregateAs, path: `.agents/skills/${aggregateAs}/SKILL.md`, aggregateAs, required: true, registryPath: manifestPath, manifestBytes: bytes, manifestSha256: sha, summary });
|
items.push({ name: aggregateAs, path: relativeManifestPath, aggregateAs, required: true, registryPath: manifestPath, manifestBytes: bytes, manifestSha256: sha, summary, sourceBundle });
|
||||||
eventItems.push({ name: aggregateAs, path: `.agents/skills/${aggregateAs}/SKILL.md`, aggregateAs, required: true, status: "materialized", manifestSha256: sha, manifestBytes: bytes, registryPath: pathSummary(manifestPath), summary, valuesPrinted: false });
|
eventItems.push({ name: aggregateAs, path: relativeManifestPath, aggregateAs, required: true, status: "materialized", manifestSha256: sha, manifestBytes: bytes, registryPath: pathSummary(manifestPath), sourceBundle, summary, valuesPrinted: false });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
@@ -325,6 +330,70 @@ async function discoverGitBundleSkills(workspacePath: string): Promise<{ items:
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function materializeRequiredSkills(refs: NonNullable<ResourceBundleRef["requiredSkills"]>, skills: MaterializedSkillRef[]): { items: MaterializedSkillRef[]; event: JsonRecord } {
|
||||||
|
if (refs.length === 0) return { items: [], event: { count: 0, materializedCount: 0, names: [], items: [], valuesPrinted: false } };
|
||||||
|
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
||||||
|
const items: MaterializedSkillRef[] = [];
|
||||||
|
const eventItems: JsonRecord[] = [];
|
||||||
|
const missing: JsonRecord[] = [];
|
||||||
|
const missingNames: string[] = [];
|
||||||
|
for (const ref of refs) {
|
||||||
|
const skill = byName.get(ref.name);
|
||||||
|
if (!skill) {
|
||||||
|
const item = { name: ref.name, path: `.agents/skills/${ref.name}/SKILL.md`, status: "missing", valuesPrinted: false };
|
||||||
|
missingNames.push(ref.name);
|
||||||
|
missing.push(item);
|
||||||
|
eventItems.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items.push(skill);
|
||||||
|
eventItems.push({ name: skill.name, path: skill.path, aggregateAs: skill.aggregateAs, status: "materialized", manifestSha256: skill.manifestSha256, manifestBytes: skill.manifestBytes, sourceBundle: skill.sourceBundle, summary: skill.summary, valuesPrinted: false });
|
||||||
|
}
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new AgentRunError("required-skill-unavailable", `required resource skill ${missingNames.join(", ")} is not materialized`, {
|
||||||
|
httpStatus: 400,
|
||||||
|
details: {
|
||||||
|
required: refs.map((ref) => ref.name),
|
||||||
|
missing,
|
||||||
|
available: skills.map((skill) => ({ name: skill.name, path: skill.path, manifestSha256: skill.manifestSha256, manifestBytes: skill.manifestBytes, sourceBundle: skill.sourceBundle, valuesPrinted: false })),
|
||||||
|
valuesPrinted: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
event: {
|
||||||
|
count: refs.length,
|
||||||
|
materializedCount: items.length,
|
||||||
|
names: items.map((item) => item.name),
|
||||||
|
items: eventItems,
|
||||||
|
valuesPrinted: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function skillSourceBundle(relativePath: string, bundles: MaterializedGitBundle[]): JsonRecord | null {
|
||||||
|
const match = bundles
|
||||||
|
.filter((bundle) => relativePathMatchesTarget(relativePath, bundle.targetPath))
|
||||||
|
.sort((left, right) => right.targetPath.length - left.targetPath.length)[0];
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
name: match.name,
|
||||||
|
repoUrl: match.repoUrl,
|
||||||
|
commitId: match.commitId,
|
||||||
|
requestedCommitId: match.requestedCommitId,
|
||||||
|
requestedRef: match.requestedRef,
|
||||||
|
subpath: match.subpath,
|
||||||
|
targetPath: match.targetPath,
|
||||||
|
valuesPrinted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativePathMatchesTarget(relativePath: string, targetPath: string): boolean {
|
||||||
|
const normalizedTarget = targetPath.replace(/\/+$/u, "");
|
||||||
|
return relativePath === normalizedTarget || relativePath.startsWith(`${normalizedTarget}/`);
|
||||||
|
}
|
||||||
|
|
||||||
function assembleInitialPrompt(promptRefs: MaterializedPromptRef[], skills: MaterializedSkillRef[]): InitialPromptAssembly | undefined {
|
function assembleInitialPrompt(promptRefs: MaterializedPromptRef[], skills: MaterializedSkillRef[]): InitialPromptAssembly | undefined {
|
||||||
if (promptRefs.length === 0 && skills.length === 0) return undefined;
|
if (promptRefs.length === 0 && skills.length === 0) return undefined;
|
||||||
const sections: string[] = [
|
const sections: string[] = [
|
||||||
|
|||||||
+17
-5
@@ -30,6 +30,13 @@ interface CommandExecutionResult extends JsonRecord {
|
|||||||
failureKind: FailureKind | null;
|
failureKind: FailureKind | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RunnerFailure {
|
||||||
|
terminalStatus: TerminalStatus;
|
||||||
|
failureKind: FailureKind;
|
||||||
|
message: string;
|
||||||
|
details?: JsonRecord | null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||||
const api = new RunnerManagerApi(options.managerUrl);
|
const api = new RunnerManagerApi(options.managerUrl);
|
||||||
const targetRun = await api.getRun(options.runId);
|
const targetRun = await api.getRun(options.runId);
|
||||||
@@ -70,7 +77,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
|||||||
let resourceEnv: NodeJS.ProcessEnv | undefined;
|
let resourceEnv: NodeJS.ProcessEnv | undefined;
|
||||||
let initialPrompt: InitialPromptAssembly | undefined;
|
let initialPrompt: InitialPromptAssembly | undefined;
|
||||||
let materializationAttempted = false;
|
let materializationAttempted = false;
|
||||||
let materializationFailure: { failureKind: FailureKind; terminalStatus: TerminalStatus; message: string } | null = null;
|
let materializationFailure: RunnerFailure | null = null;
|
||||||
let backendSession: BackendSession | null = null;
|
let backendSession: BackendSession | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -104,7 +111,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failureKind = failureKindFromError(error);
|
const failureKind = failureKindFromError(error);
|
||||||
materializationFailure = { failureKind, terminalStatus: terminalStatusForFailure(failureKind), message: errorMessage(error) };
|
materializationFailure = { failureKind, terminalStatus: terminalStatusForFailure(failureKind), message: errorMessage(error), details: failureDetailsFromError(error) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +314,10 @@ async function reportNonTerminalCommandFailure(api: RunnerManagerApi, runId: str
|
|||||||
await api.reportCommandStatus(commandId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message, ...(control ? { threadId: control.threadId, turnId: control.turnId } : {}) });
|
await api.reportCommandStatus(commandId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message, ...(control ? { threadId: control.threadId, turnId: control.turnId } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function failureDetailsFromError(error: unknown): JsonRecord | null {
|
||||||
|
return error instanceof AgentRunError ? error.details : null;
|
||||||
|
}
|
||||||
|
|
||||||
function steerPrompt(payload: JsonRecord): string | null {
|
function steerPrompt(payload: JsonRecord): string | null {
|
||||||
for (const key of ["prompt", "message", "text"]) {
|
for (const key of ["prompt", "message", "text"]) {
|
||||||
const value = payload[key];
|
const value = payload[key];
|
||||||
@@ -466,9 +477,10 @@ function annotateCommandEvent(event: BackendEvent, commandId: string, attemptId:
|
|||||||
return { ...event, payload: { ...event.payload, commandId, attemptId, runnerId } };
|
return { ...event, payload: { ...event.payload, commandId, attemptId, runnerId } };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reportCommandFailure(api: RunnerManagerApi, runId: string, commandId: string, runner: RunnerRecord, attemptId: string, failure: { terminalStatus: TerminalStatus; failureKind: FailureKind; message: string }, phase: string, options: { terminalRun?: boolean } = {}): Promise<CommandExecutionResult> {
|
async function reportCommandFailure(api: RunnerManagerApi, runId: string, commandId: string, runner: RunnerRecord, attemptId: string, failure: RunnerFailure, phase: string, options: { terminalRun?: boolean } = {}): Promise<CommandExecutionResult> {
|
||||||
await api.appendEvent(runId, { type: "error", payload: { failureKind: failure.failureKind, message: failure.message, phase, commandId, attemptId, runnerId: runner.id } });
|
const details = failure.details ? { details: failure.details } : {};
|
||||||
await api.appendEvent(runId, { type: "terminal_status", payload: { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, message: failure.message, commandId, attemptId, runnerId: runner.id } });
|
await api.appendEvent(runId, { type: "error", payload: { failureKind: failure.failureKind, message: failure.message, phase, commandId, attemptId, runnerId: runner.id, ...details } });
|
||||||
|
await api.appendEvent(runId, { type: "terminal_status", payload: { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, message: failure.message, commandId, attemptId, runnerId: runner.id, ...details } });
|
||||||
await api.reportCommandStatus(commandId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message });
|
await api.reportCommandStatus(commandId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message });
|
||||||
if (options.terminalRun === true) await api.reportStatus(runId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message });
|
if (options.terminalRun === true) await api.reportStatus(runId, { terminalStatus: failure.terminalStatus, failureKind: failure.failureKind, failureMessage: failure.message });
|
||||||
return { commandId, terminalStatus: failure.terminalStatus, failureKind: failure.failureKind } as CommandExecutionResult;
|
return { commandId, terminalStatus: failure.terminalStatus, failureKind: failure.failureKind } as CommandExecutionResult;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { JsonRecord, ResourceBundleRef } from "../../common/types.js";
|
|||||||
import { assertNoSecretLeak, type SelfTestCase, type SelfTestContext } from "../harness.js";
|
import { assertNoSecretLeak, type SelfTestCase, type SelfTestContext } from "../harness.js";
|
||||||
|
|
||||||
const execFile = promisify(execFileCallback);
|
const execFile = promisify(execFileCallback);
|
||||||
type LocalBundle = { repoUrl: string; commitId: string; branch?: string; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"] };
|
type LocalBundle = { repoUrl: string; commitId: string; branch?: string; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"]; requiredSkills?: ResourceBundleRef["requiredSkills"] };
|
||||||
type RefResolvedBundle = LocalBundle & { branch: string; latestCommitId: string };
|
type RefResolvedBundle = LocalBundle & { branch: string; latestCommitId: string };
|
||||||
|
|
||||||
const selfTest: SelfTestCase = async (context) => {
|
const selfTest: SelfTestCase = async (context) => {
|
||||||
@@ -93,12 +93,19 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
assert.equal(((resultEnvelope.sessionRef as JsonRecord).threadId), "thread_selftest_1");
|
assert.equal(((resultEnvelope.sessionRef as JsonRecord).threadId), "thread_selftest_1");
|
||||||
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).commitId), bundle.commitId);
|
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).commitId), bundle.commitId);
|
||||||
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).kind), "gitbundle");
|
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).kind), "gitbundle");
|
||||||
|
assert.deepEqual((((resultEnvelope.resourceBundleRef as JsonRecord).requiredSkills as JsonRecord).names), ["dad-dev"]);
|
||||||
const resultBundleTargets = (((resultEnvelope.resourceBundleRef as JsonRecord).bundles as JsonRecord).items as JsonRecord[]).map((item) => item.targetPath);
|
const resultBundleTargets = (((resultEnvelope.resourceBundleRef as JsonRecord).bundles as JsonRecord).items as JsonRecord[]).map((item) => item.targetPath);
|
||||||
assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]);
|
assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]);
|
||||||
const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord);
|
const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord);
|
||||||
assert.deepEqual(((materialized.tools as JsonRecord).names), ["hwpod"]);
|
assert.deepEqual(((materialized.tools as JsonRecord).names), ["hwpod"]);
|
||||||
assert.equal(((materialized.tools as JsonRecord).installed), true);
|
assert.equal(((materialized.tools as JsonRecord).installed), true);
|
||||||
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]);
|
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
|
||||||
|
const requiredSkillItems = ((materialized.requiredSkills as JsonRecord).items as JsonRecord[]);
|
||||||
|
assert.deepEqual(((materialized.requiredSkills as JsonRecord).names), ["dad-dev"]);
|
||||||
|
assert.equal(requiredSkillItems[0]?.status, "materialized");
|
||||||
|
assert.equal(typeof requiredSkillItems[0]?.manifestSha256, "string");
|
||||||
|
assert.equal(typeof requiredSkillItems[0]?.manifestBytes, "number");
|
||||||
|
assert.equal(((requiredSkillItems[0]?.sourceBundle as JsonRecord).commitId), bundle.commitId);
|
||||||
assertNoSecretLeak(resultEnvelope);
|
assertNoSecretLeak(resultEnvelope);
|
||||||
|
|
||||||
const refBundle = await createRefResolvedLocalGitBundle(context);
|
const refBundle = await createRefResolvedLocalGitBundle(context);
|
||||||
@@ -129,6 +136,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
const firstAssemblyInput = turnInputText(assemblyInputs[0]);
|
const firstAssemblyInput = turnInputText(assemblyInputs[0]);
|
||||||
const secondAssemblyInput = turnInputText(assemblyInputs[1]);
|
const secondAssemblyInput = turnInputText(assemblyInputs[1]);
|
||||||
assert.match(firstAssemblyInput, /HWLAB v0\.2 runtime prompt self-test/u);
|
assert.match(firstAssemblyInput, /HWLAB v0\.2 runtime prompt self-test/u);
|
||||||
|
assert.match(firstAssemblyInput, /dad-dev/u);
|
||||||
assert.match(firstAssemblyInput, /hwpod-cli/u);
|
assert.match(firstAssemblyInput, /hwpod-cli/u);
|
||||||
assert.match(firstAssemblyInput, /hwpod-ctl/u);
|
assert.match(firstAssemblyInput, /hwpod-ctl/u);
|
||||||
assert.match(firstAssemblyInput, /hwpod-compiler-cli/u);
|
assert.match(firstAssemblyInput, /hwpod-compiler-cli/u);
|
||||||
@@ -149,7 +157,8 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
assert.deepEqual(assemblyBundleTargets, ["tools", ".agents/skills"]);
|
assert.deepEqual(assemblyBundleTargets, ["tools", ".agents/skills"]);
|
||||||
const assemblyMaterialized = assemblyResource.materialized as JsonRecord;
|
const assemblyMaterialized = assemblyResource.materialized as JsonRecord;
|
||||||
assert.deepEqual(((assemblyMaterialized.promptRefs as JsonRecord).names), ["hwlab-v02-runtime"]);
|
assert.deepEqual(((assemblyMaterialized.promptRefs as JsonRecord).names), ["hwlab-v02-runtime"]);
|
||||||
assert.deepEqual(((assemblyMaterialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]);
|
assert.deepEqual(((assemblyMaterialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
|
||||||
|
assert.deepEqual(((assemblyMaterialized.requiredSkills as JsonRecord).names), ["dad-dev"]);
|
||||||
assert.equal(((assemblyMaterialized.initialPrompt as JsonRecord).available), true);
|
assert.equal(((assemblyMaterialized.initialPrompt as JsonRecord).available), true);
|
||||||
assertNoSecretLeak(assemblyEnvelope);
|
assertNoSecretLeak(assemblyEnvelope);
|
||||||
|
|
||||||
@@ -158,6 +167,21 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
assert.equal(missingPromptResult.terminalStatus, "blocked");
|
assert.equal(missingPromptResult.terminalStatus, "blocked");
|
||||||
assert.equal(missingPromptResult.failureKind, "prompt-unavailable");
|
assert.equal(missingPromptResult.failureKind, "prompt-unavailable");
|
||||||
|
|
||||||
|
const missingSkillRun = await createHwlabRun(client, context, { ...bundle, bundles: [{ name: "hwlab-tools", subpath: "tools", targetPath: "tools" }], requiredSkills: [{ name: "dad-dev" }] }, "hwlab-session-missing-skill", "missing required skill", "hwlab-command-missing-skill");
|
||||||
|
const missingSkillResult = await runOnce({ managerUrl: server.baseUrl, runId: missingSkillRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-missing-skill") }, oneShot: true }) as JsonRecord;
|
||||||
|
assert.equal(missingSkillResult.terminalStatus, "blocked");
|
||||||
|
assert.equal(missingSkillResult.failureKind, "required-skill-unavailable");
|
||||||
|
const missingSkillEnvelope = await client.get(`/api/v1/runs/${missingSkillRun.runId}/commands/${missingSkillRun.commandId}/result`) as JsonRecord;
|
||||||
|
assert.equal(missingSkillEnvelope.terminalStatus, "blocked");
|
||||||
|
assert.equal(missingSkillEnvelope.terminalSource, "terminal_status-event");
|
||||||
|
assert.equal(missingSkillEnvelope.failureKind, "required-skill-unavailable");
|
||||||
|
assert.deepEqual((((missingSkillEnvelope.resourceBundleRef as JsonRecord).requiredSkills as JsonRecord).names), ["dad-dev"]);
|
||||||
|
const missingSkillBlocker = missingSkillEnvelope.blocker as JsonRecord;
|
||||||
|
assert.equal(missingSkillBlocker.failureKind, "required-skill-unavailable");
|
||||||
|
assert.deepEqual(((missingSkillBlocker.details as JsonRecord).required as string[]), ["dad-dev"]);
|
||||||
|
assert.deepEqual((((missingSkillBlocker.details as JsonRecord).missing as JsonRecord[]).map((item) => item.name)), ["dad-dev"]);
|
||||||
|
assertNoSecretLeak(missingSkillEnvelope);
|
||||||
|
|
||||||
const resumed = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello resumed", "hwlab-command-session-resumed");
|
const resumed = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello resumed", "hwlab-command-session-resumed");
|
||||||
const resumedRun = await client.get(`/api/v1/runs/${resumed.runId}`) as JsonRecord;
|
const resumedRun = await client.get(`/api/v1/runs/${resumed.runId}`) as JsonRecord;
|
||||||
assert.equal(((resumedRun.sessionRef as JsonRecord).threadId), "thread_selftest_1");
|
assert.equal(((resumedRun.sessionRef as JsonRecord).threadId), "thread_selftest_1");
|
||||||
@@ -237,7 +261,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
|||||||
const runningResult = await running;
|
const runningResult = await running;
|
||||||
assert.equal(runningResult.terminalStatus, "cancelled");
|
assert.equal(runningResult.terminalStatus, "cancelled");
|
||||||
|
|
||||||
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-gitbundle-materialization", "gitbundle-ref-resolution", "gitbundle-tools-path", "gitbundle-skill-dir-assembly", "resource-prompt-required-blocker", "same-run-runner-multiturn", "running-steer", "idle-after-tool-liveness", "running-cancel"] };
|
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-gitbundle-materialization", "gitbundle-ref-resolution", "gitbundle-tools-path", "gitbundle-skill-dir-assembly", "resource-prompt-required-blocker", "resource-required-skill-blocker", "same-run-runner-multiturn", "running-steer", "idle-after-tool-liveness", "running-cancel"] };
|
||||||
} finally {
|
} finally {
|
||||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||||
}
|
}
|
||||||
@@ -262,6 +286,15 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
|
|||||||
"Compile D601-F103-V2 through hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node.",
|
"Compile D601-F103-V2 through hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node.",
|
||||||
"Do not invent fallback hardware paths or legacy profile routes.",
|
"Do not invent fallback hardware paths or legacy profile routes.",
|
||||||
].join("\n"), "utf8");
|
].join("\n"), "utf8");
|
||||||
|
await mkdir(path.join(repo, "skills", "dad-dev"), { recursive: true });
|
||||||
|
await writeFile(path.join(repo, "skills", "dad-dev", "SKILL.md"), [
|
||||||
|
"---",
|
||||||
|
"name: dad-dev",
|
||||||
|
"description: Track dad-dev issue progress and validate delivery evidence for AgentRun work.",
|
||||||
|
"---",
|
||||||
|
"# dad-dev",
|
||||||
|
"Record P1-P4 evidence and remaining risk for AgentRun issue work.",
|
||||||
|
].join("\n"), "utf8");
|
||||||
await mkdir(path.join(repo, "skills", "hwpod-cli", "scripts"), { recursive: true });
|
await mkdir(path.join(repo, "skills", "hwpod-cli", "scripts"), { recursive: true });
|
||||||
await writeFile(path.join(repo, "skills", "hwpod-cli", "SKILL.md"), [
|
await writeFile(path.join(repo, "skills", "hwpod-cli", "SKILL.md"), [
|
||||||
"---",
|
"---",
|
||||||
@@ -282,10 +315,10 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
|
|||||||
"Use hwpod-ctl for HWPOD runtime inspection and control-plane state.",
|
"Use hwpod-ctl for HWPOD runtime inspection and control-plane state.",
|
||||||
].join("\n"), "utf8");
|
].join("\n"), "utf8");
|
||||||
await writeFile(path.join(repo, "skills", "hwpod-ctl", "scripts", "hwpod-ctl.mjs"), "console.log(JSON.stringify({ ok: true, cli: 'hwpod-ctl-skill-selftest' }));\n", "utf8");
|
await writeFile(path.join(repo, "skills", "hwpod-ctl", "scripts", "hwpod-ctl.mjs"), "console.log(JSON.stringify({ ok: true, cli: 'hwpod-ctl-skill-selftest' }));\n", "utf8");
|
||||||
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
|
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
|
||||||
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
|
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
|
||||||
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
|
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
|
||||||
return { repoUrl: repo, commitId: stdout.trim() };
|
return { repoUrl: repo, commitId: stdout.trim(), requiredSkills: [{ name: "dad-dev" }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promise<RefResolvedBundle> {
|
async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promise<RefResolvedBundle> {
|
||||||
@@ -304,6 +337,7 @@ async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promis
|
|||||||
async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: LocalBundle, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> {
|
async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: LocalBundle, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> {
|
||||||
const resourceBundleRef: ResourceBundleRef = { kind: "gitbundle", repoUrl: bundle.repoUrl, commitId: bundle.commitId, bundles: bundle.bundles ?? defaultGitBundles(), submodules: false, lfs: false };
|
const resourceBundleRef: ResourceBundleRef = { kind: "gitbundle", repoUrl: bundle.repoUrl, commitId: bundle.commitId, bundles: bundle.bundles ?? defaultGitBundles(), submodules: false, lfs: false };
|
||||||
if (bundle.promptRefs) resourceBundleRef.promptRefs = bundle.promptRefs;
|
if (bundle.promptRefs) resourceBundleRef.promptRefs = bundle.promptRefs;
|
||||||
|
if (bundle.requiredSkills) resourceBundleRef.requiredSkills = bundle.requiredSkills;
|
||||||
const run = await client.post("/api/v1/runs", {
|
const run = await client.post("/api/v1/runs", {
|
||||||
tenantId: "hwlab",
|
tenantId: "hwlab",
|
||||||
projectId: "pikasTech/HWLAB",
|
projectId: "pikasTech/HWLAB",
|
||||||
|
|||||||
Reference in New Issue
Block a user