feat: add agnet artifact content proxy
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Heicode Manager 可独立执行任务计划
|
# Heicode Manager 可独立执行任务计划
|
||||||
|
|
||||||
更新时间:2026-05-27
|
更新时间:2026-05-30
|
||||||
负责人范围:Heicode Manager 端
|
负责人范围:Heicode Manager 端
|
||||||
用途:后续开发按本文逐项执行、验收和更新状态。
|
用途:后续开发按本文逐项执行、验收和更新状态。
|
||||||
|
|
||||||
@@ -47,6 +47,8 @@
|
|||||||
| 12 | 文档口径清理 | P2 | 是 | 已完成:当前口径以 Azure Key Vault / secret_ref 为准 |
|
| 12 | 文档口径清理 | P2 | 是 | 已完成:当前口径以 Azure Key Vault / secret_ref 为准 |
|
||||||
| 13 | AWS/GCP 占位提示 | P2 | 是 | 已完成:避免用户误以为 AWS/GCP 已可用 |
|
| 13 | AWS/GCP 占位提示 | P2 | 是 | 已完成:避免用户误以为 AWS/GCP 已可用 |
|
||||||
| 14 | 请求 body 加密策略确认 | P2 | 是 | 已完成:桌面端 sub POST 复用模型调用 V2 body 加密 |
|
| 14 | 请求 body 加密策略确认 | P2 | 是 | 已完成:桌面端 sub POST 复用模型调用 V2 body 加密 |
|
||||||
|
| 15 | Runtime 状态诊断与兜底产物识别 | P1 | 是 | 已完成:Manager 可独立查询 Runtime 状态,页面明确区分普通 sub / 蜂群,并提示兜底摘要不是最终交付物 |
|
||||||
|
| 16 | Artifact 完整内容代理下载 | P1 | 是 | 已完成:Manager 校验用户和 artifact 后,通过 Runtime content 接口代理下载完整产物 |
|
||||||
|
|
||||||
## 四、任务明细
|
## 四、任务明细
|
||||||
|
|
||||||
@@ -273,6 +275,43 @@
|
|||||||
- 服务端日志不打印请求 body 中的敏感字段。
|
- 服务端日志不打印请求 body 中的敏感字段。
|
||||||
- V2 加密失败时返回 `X-Heicode-Auth-Error` 和 `X-Heicode-Server-Time`,便于客户端排障。
|
- V2 加密失败时返回 `X-Heicode-Auth-Error` 和 `X-Heicode-Server-Time`,便于客户端排障。
|
||||||
|
|
||||||
|
### 任务 15:Runtime 状态诊断与兜底产物识别
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| 目标 | Manager 不依赖客户端或 Runtime 改接口,也能把“callback 已到”和“Runtime/Agent 是否真的产出交付物”分开展示 |
|
||||||
|
| 修改文件 | `heicode/controller/agnet_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agnet-console/api.ts`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
|
||||||
|
| 新增 API | `GET /api/agnet/user/deployments/:deployment_id/runtime-diagnostics` |
|
||||||
|
| 诊断来源 | 用 deployment 记录里的 `runtime_swarm_id` / `runtime_deployment_id` 查询 Runtime status;默认路径 `/api/swarms/{swarm_id}/status`,可用 `AGNET_RUNTIME_STATUS_PATH` / `SWARM_RUNTIME_STATUS_PATH` 配置 |
|
||||||
|
| 普通 sub / 蜂群边界 | 返回 `runtime_mode`,页面分别显示“普通 sub 模式”或“蜂群模式”,不把二者合并成一个流程 |
|
||||||
|
| 已识别异常 | Runtime agent failed、completed 但存在 failed agents、只有 `Runtime execution summary` 兜底产物、模型 token 用量为 0、Runtime 状态查询失败 |
|
||||||
|
| 测试 | `go test ./controller -run TestAgnetRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents -count=1`;前端 `bun run build:check` |
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 诊断接口只读,不改 deployment 状态,不伪造 artifact。
|
||||||
|
- 页面中的兜底摘要必须明确提示“不是最终业务交付物”。
|
||||||
|
- 普通 sub 与蜂群模式必须通过 `runtime_mode` 区分展示。
|
||||||
|
- Runtime 状态查不到时返回可读 warning,不能影响 Manager 已落库 callback/timeline/artifact 查询。
|
||||||
|
|
||||||
|
### 任务 16:Artifact 完整内容代理下载
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| 目标 | 对齐 Agent Manager v2.1.10 的产物获取流程:Manager / 前端先查 artifact 列表,再通过用户态 content 代理接口获取完整文件 |
|
||||||
|
| 修改文件 | `heicode/model/agnet_artifact.go`、`heicode/controller/agnet_callback.go`、`heicode/controller/agnet_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
|
||||||
|
| 新增 API | `GET /api/agnet/user/deployments/:deployment_id/artifacts/:artifact_id/content` |
|
||||||
|
| Runtime 目标路径 | 默认 `GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content`,可用 `AGNET_RUNTIME_ARTIFACT_CONTENT_PATH` / `SWARM_RUNTIME_ARTIFACT_CONTENT_PATH` 配置 |
|
||||||
|
| 安全边界 | Manager 先校验当前用户拥有 deployment,再校验 artifact 属于该 deployment;不暴露 Azure Blob 凭据、SAS URL 或 Runtime 内网信息 |
|
||||||
|
| 测试 | `go test ./controller -run TestAgnetArtifactContentProxiesRuntimeContent -count=1` |
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 不能直接用用户传入的 URI 下载,必须以 Manager 已落库 artifact 为准。
|
||||||
|
- 响应透传 Runtime 的文件正文、`Content-Type` 和 `Content-Disposition`。
|
||||||
|
- Runtime 未配置或 content 拉取失败时返回明确业务错误。
|
||||||
|
- 页面 artifact 卡片提供“下载产物”,但兜底摘要仍需标记为非最终业务交付物。
|
||||||
|
|
||||||
## 五、推荐执行批次
|
## 五、推荐执行批次
|
||||||
|
|
||||||
### 批次 A:最小 Manager 闭环
|
### 批次 A:最小 Manager 闭环
|
||||||
@@ -307,6 +346,8 @@
|
|||||||
| 4 | 文档口径清理 |
|
| 4 | 文档口径清理 |
|
||||||
| 5 | AWS/GCP 占位提示 |
|
| 5 | AWS/GCP 占位提示 |
|
||||||
| 6 | 请求 body 加密策略确认 |
|
| 6 | 请求 body 加密策略确认 |
|
||||||
|
| 7 | Runtime 状态诊断与兜底产物识别 |
|
||||||
|
| 8 | Artifact 完整内容代理下载 |
|
||||||
|
|
||||||
完成批次 C 后,Manager 端应具备清晰的任务视角、持久化上下文、准确页面口径和更少误导。
|
完成批次 C 后,Manager 端应具备清晰的任务视角、持久化上下文、准确页面口径和更少误导。
|
||||||
|
|
||||||
@@ -356,6 +397,8 @@ Manager 独立任务完成,不等于蜂群生产闭环完成。本文完成的
|
|||||||
| 审计 | task/deployment/correlation_id 下能聚合审计、审批、artifact、callback |
|
| 审计 | task/deployment/correlation_id 下能聚合审计、审批、artifact、callback |
|
||||||
| 安全 | API、日志、页面不出现明文长期密钥 |
|
| 安全 | API、日志、页面不出现明文长期密钥 |
|
||||||
| 请求加密 | 桌面端 sub 请求支持与模型调用一致的 V2 body 加密;浏览器后台普通 JSON 兼容路径不受影响 |
|
| 请求加密 | 桌面端 sub 请求支持与模型调用一致的 V2 body 加密;浏览器后台普通 JSON 兼容路径不受影响 |
|
||||||
|
| Runtime 诊断 | Manager 页面能显示 Runtime 真实状态来源、失败 Agent、兜底产物和普通 sub / 蜂群模式归属 |
|
||||||
|
| Artifact 内容 | Manager 用户态接口能代理下载 Runtime 保存的完整 artifact 内容,且不暴露存储凭据 |
|
||||||
| 口径 | 页面和文档不把本地占位/模拟事件说成真实 Runtime |
|
| 口径 | 页面和文档不把本地占位/模拟事件说成真实 Runtime |
|
||||||
|
|
||||||
## 八、执行时不能突破的边界
|
## 八、执行时不能突破的边界
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Heicode Manager 蜂群模式进度清单
|
# Heicode Manager 普通 sub 与蜂群模式进度清单
|
||||||
|
|
||||||
更新时间:2026-05-28
|
更新时间:2026-05-30
|
||||||
负责人范围:Heicode Manager 端
|
负责人范围:Heicode Manager 端
|
||||||
用途:给负责人、上级和联调同学快速确认 Manager 端在蜂群模式下已经具备什么、还要做什么、哪些需要客户端或蜂群项目配合。
|
用途:给负责人、上级和联调同学快速确认 Manager 端在普通 sub 与蜂群模式下已经具备什么、还要做什么、哪些需要客户端或 Agent Manager / 蜂群项目配合。
|
||||||
|
|
||||||
## 资料来源
|
## 资料来源
|
||||||
|
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
|
|
||||||
蜂群模式不是普通 sub 敏捷/瀑布本身。普通 sub 是 Heicode 的任务组织方式;蜂群是 Agnet/Swarm Runtime 的执行方式。
|
蜂群模式不是普通 sub 敏捷/瀑布本身。普通 sub 是 Heicode 的任务组织方式;蜂群是 Agnet/Swarm Runtime 的执行方式。
|
||||||
|
|
||||||
|
| 模式 | Manager 当前职责 | Runtime / Agent Manager 当前职责 | 不能混淆的点 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 普通 sub 敏捷/瀑布 | 从任务卡生成 deployment、保存 `sub_mode`、权限清单、预算、callback、timeline、artifact、审批记录和运行时诊断 | 执行普通 sub 任务,回传阶段、日志、工具调用、artifact、usage、失败原因 | 普通 sub 不等于蜂群 task graph;`/api/swarms` 可作为 Runtime 兼容入口,但页面和文档必须按普通 sub 展示 |
|
||||||
|
| 蜂群模式 | 生成 swarm adapter 请求、保存 `deployment_id <-> swarm_id` 映射、接回调、审批、审计、artifact 展示和运行时诊断 | 创建 Swarm Run、任务图、claim、heartbeat、handoff、Agent 编队、真实执行与结果回传 | 蜂群是执行形态;不能把普通 sub 的阶段状态误写成蜂群已完成 |
|
||||||
|
|
||||||
| 系统 | 定位 | 应该做什么 | 不应该做什么 |
|
| 系统 | 定位 | 应该做什么 | 不应该做什么 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Heicode 桌面客户端 | 用户主体验 | 输入目标、持续补充需求、查看反馈、审批高危操作、接收交付结果 | 直接配置 AKS、模型供应商、完整蜂群 payload |
|
| Heicode 桌面客户端 | 用户主体验 | 输入目标、持续补充需求、查看反馈、审批高危操作、接收交付结果 | 直接配置 AKS、模型供应商、完整蜂群 payload |
|
||||||
@@ -55,10 +60,12 @@ Heicode 桌面客户端
|
|||||||
| `/api/swarms` 兼容入口 | 已有用户态 `POST /api/swarms`,内部走 Manager deployment 创建,并作为 adapter source 记录 | `heicode/router/api-router.go`、`AgnetCreateUserSwarm` |
|
| `/api/swarms` 兼容入口 | 已有用户态 `POST /api/swarms`,内部走 Manager deployment 创建,并作为 adapter source 记录 | `heicode/router/api-router.go`、`AgnetCreateUserSwarm` |
|
||||||
| Runtime 创建桥接 | 已能按配置调用 Runtime 创建接口,默认路径 `/api/agnet/deployments`,可用环境变量改为蜂群创建路径 | `heicode/controller/agnet_runtime_client.go` |
|
| Runtime 创建桥接 | 已能按配置调用 Runtime 创建接口,默认路径 `/api/agnet/deployments`,可用环境变量改为蜂群创建路径 | `heicode/controller/agnet_runtime_client.go` |
|
||||||
| Runtime stop 桥接 | 已能在停止 Manager deployment 时调用 Runtime stop | `heicode/controller/agnet_runtime_client.go` |
|
| Runtime stop 桥接 | 已能在停止 Manager deployment 时调用 Runtime stop | `heicode/controller/agnet_runtime_client.go` |
|
||||||
|
| Runtime 状态诊断 | 已新增用户态只读诊断接口,按 `runtime_mode` 区分普通 sub / 蜂群,查询 Runtime status 并识别 failed agent、兜底摘要 artifact、零 token 用量等异常 | `heicode/controller/agnet_runtime_client.go`、`AgnetGetUserDeploymentRuntimeDiagnostics` |
|
||||||
| callback 接收 | 已有 `POST /api/agnet/callbacks/swarm-events` | `heicode/controller/agnet_callback.go` |
|
| callback 接收 | 已有 `POST /api/agnet/callbacks/swarm-events` | `heicode/controller/agnet_callback.go` |
|
||||||
| callback 鉴权 | 支持 `X-Agnet-Service-Token` 和 HMAC 签名校验,并可从 Key Vault ref 读取签名密钥 | `heicode/controller/agnet_callback.go` |
|
| callback 鉴权 | 支持 `X-Agnet-Service-Token` 和 HMAC 签名校验,并可从 Key Vault ref 读取签名密钥 | `heicode/controller/agnet_callback.go` |
|
||||||
| callback 幂等 | `event_id` / `idempotency_key` 去重,重复回调返回成功但不重复写 | `heicode/model/agnet_callback.go` |
|
| callback 幂等 | `event_id` / `idempotency_key` 去重,重复回调返回成功但不重复写 | `heicode/model/agnet_callback.go` |
|
||||||
| artifact 落库 | `artifact.created` 可生成 artifact 记录,支持用户态列表查询 | `heicode/model/agnet_artifact.go`、`AgnetListUserDeploymentArtifacts` |
|
| artifact 落库 | `artifact.created` 可生成 artifact 记录,支持用户态列表查询 | `heicode/model/agnet_artifact.go`、`AgnetListUserDeploymentArtifacts` |
|
||||||
|
| artifact 完整内容代理 | 已新增用户态 content 下载接口,Manager 校验 deployment/artifact 权限后代理 Runtime content 接口读取完整产物 | `AgnetGetUserDeploymentArtifactContent`、`callAgnetRuntimeArtifactContent` |
|
||||||
| approval 回调 | `approval.requested` 可转成 Manager 审批记录 | `heicode/controller/agnet_callback.go` |
|
| approval 回调 | `approval.requested` 可转成 Manager 审批记录 | `heicode/controller/agnet_callback.go` |
|
||||||
| 审批结果回传 Runtime | 用户 approve/reject 后,Manager 可按配置 POST 回 Runtime approval decision,且不发送 `secret_ref` | `heicode/controller/agnet_approval.go`、`heicode/controller/agnet_runtime_client.go` |
|
| 审批结果回传 Runtime | 用户 approve/reject 后,Manager 可按配置 POST 回 Runtime approval decision,且不发送 `secret_ref` | `heicode/controller/agnet_approval.go`、`heicode/controller/agnet_runtime_client.go` |
|
||||||
| timeline 聚合 | 用户态 timeline 聚合 audit、callbacks、artifacts、sk_snapshots | `AgnetGetUserDeploymentTimeline` |
|
| timeline 聚合 | 用户态 timeline 聚合 audit、callbacks、artifacts、sk_snapshots | `AgnetGetUserDeploymentTimeline` |
|
||||||
@@ -78,7 +85,9 @@ Heicode 桌面客户端
|
|||||||
| P0 | Runtime 联调配置模板 | 已完成:两份 Agent Manager 对接任务清单已写清 `AGNET_RUNTIME_*`、callback URL、service token/HMAC 方式和验收步骤 | 是 | 蜂群项目按模板能调用 Manager callback |
|
| P0 | Runtime 联调配置模板 | 已完成:两份 Agent Manager 对接任务清单已写清 `AGNET_RUNTIME_*`、callback URL、service token/HMAC 方式和验收步骤 | 是 | 蜂群项目按模板能调用 Manager callback |
|
||||||
| P1 | 审批结果回传 Runtime 联调 | Manager adapter 已有;仍需要 Runtime 提供接收接口并验证状态继续/停止 | 需要 Runtime 接口 | 审批通过/拒绝后 Runtime 状态能继续或停止 |
|
| P1 | 审批结果回传 Runtime 联调 | Manager adapter 已有;仍需要 Runtime 提供接收接口并验证状态继续/停止 | 需要 Runtime 接口 | 审批通过/拒绝后 Runtime 状态能继续或停止 |
|
||||||
| P1 | Artifact 展示优化 | Manager 端已完成:页面展示 artifact 类型、摘要和 URI;真实 `code_patch/document/test_report/deployment_manifest` 仍需 Runtime 输出 | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 |
|
| P1 | Artifact 展示优化 | Manager 端已完成:页面展示 artifact 类型、摘要和 URI;真实 `code_patch/document/test_report/deployment_manifest` 仍需 Runtime 输出 | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 |
|
||||||
|
| P1 | Artifact 完整内容下载 | 已完成:用户态 `/artifacts/{artifact_id}/content` 代理 Runtime content,页面提供下载入口 | 是 | artifact 属于当前用户 deployment 才能下载,响应透传 Runtime 文件内容 |
|
||||||
| P1 | 任务图/Agent 状态展示占位 | 已完成:页面从 `task.*` / `handoff.*` callback 聚合 Agent task map;无真实数据时显示 Runtime callback 空态 | Manager 可先做展示结构,真实数据需 Runtime | 有空态和字段,不宣称真实已运行 |
|
| P1 | 任务图/Agent 状态展示占位 | 已完成:页面从 `task.*` / `handoff.*` callback 聚合 Agent task map;无真实数据时显示 Runtime callback 空态 | Manager 可先做展示结构,真实数据需 Runtime | 有空态和字段,不宣称真实已运行 |
|
||||||
|
| P1 | Runtime 状态诊断展示 | 已完成:任务总览详情页显示运行模式、运行状态、数据来源和异常 warning;兜底摘要产物会提示“不是最终业务交付物” | 是 | 页面能识别 callback 已到但 Runtime agent 失败、只返回兜底摘要的情况 |
|
||||||
| P1 | 日志/指标真实来源标识 | 已完成:logs/metrics API 返回 `data_source`、`runtime_source`,当前明确是 Manager control-plane / estimated,不伪装 Runtime 真实指标 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 |
|
| P1 | 日志/指标真实来源标识 | 已完成:logs/metrics API 返回 `data_source`、`runtime_source`,当前明确是 Manager control-plane / estimated,不伪装 Runtime 真实指标 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 |
|
||||||
| P1 | Agent 运行费用口径收敛 | 已完成文档口径:`budget.max_tokens/max_cost_usd/max_duration_sec` 是预算约束,不等于真实扣费账本;真实收费必须依赖 Runtime 回传 usage | Manager 已完成文档,真实数据需 Runtime | 页面/文档不把 estimated budget 说成真实扣费 |
|
| P1 | Agent 运行费用口径收敛 | 已完成文档口径:`budget.max_tokens/max_cost_usd/max_duration_sec` 是预算约束,不等于真实扣费账本;真实收费必须依赖 Runtime 回传 usage | Manager 已完成文档,真实数据需 Runtime | 页面/文档不把 estimated budget 说成真实扣费 |
|
||||||
| P1 | 高危审批客户端联动文档 | 已完成:`docs/integration/heicode-desktop-sub-agile-api.md` 已包含 approval 查询、approve/reject、awaiting_approval 流程 | 是 | 客户端文档补齐 approval flow |
|
| P1 | 高危审批客户端联动文档 | 已完成:`docs/integration/heicode-desktop-sub-agile-api.md` 已包含 approval 查询、approve/reject、awaiting_approval 流程 | 是 | 客户端文档补齐 approval flow |
|
||||||
@@ -116,6 +125,7 @@ Heicode 桌面客户端
|
|||||||
| HeiCode-Swarm 项目等于正式 Heicode 桌面客户端 | 不是。它有自己的 `desktop-client` 演示端,正式链路应走 Heicode 桌面客户端 -> Manager -> Runtime |
|
| HeiCode-Swarm 项目等于正式 Heicode 桌面客户端 | 不是。它有自己的 `desktop-client` 演示端,正式链路应走 Heicode 桌面客户端 -> Manager -> Runtime |
|
||||||
| `/api/swarms` 已等于真实 Runtime Swarm Run | 不是。Manager 侧已有 adapter 入口,但是否真实创建 Swarm Run 取决于 Runtime 配置和蜂群接口 |
|
| `/api/swarms` 已等于真实 Runtime Swarm Run | 不是。Manager 侧已有 adapter 入口,但是否真实创建 Swarm Run 取决于 Runtime 配置和蜂群接口 |
|
||||||
| artifact/timeline 有接口就等于有真实产物 | 不是。Manager 能接和展示,真实产物必须由 Runtime 回调 |
|
| artifact/timeline 有接口就等于有真实产物 | 不是。Manager 能接和展示,真实产物必须由 Runtime 回调 |
|
||||||
|
| `Runtime execution summary` 就等于最终交付物 | 不是。Manager 页面会标记这是兜底摘要;真实最终交付物必须是 Runtime/Agent 返回的 `code_patch`、`document`、`test_report`、Git branch/commit、部署地址等可核对 artifact |
|
||||||
| 高危审批在 Manager 里点完就闭环 | 不是。产品要求桌面客户端主审批,并且 Runtime 要收到 decision |
|
| 高危审批在 Manager 里点完就闭环 | 不是。产品要求桌面客户端主审批,并且 Runtime 要收到 decision |
|
||||||
| 本地测试通过就等于生产接口全通 | 不是。本地 router/controller/middleware 测试能证明代码能力;生产仍必须确认对应镜像、路由和认证配置已生效 |
|
| 本地测试通过就等于生产接口全通 | 不是。本地 router/controller/middleware 测试能证明代码能力;生产仍必须确认对应镜像、路由和认证配置已生效 |
|
||||||
| Agent budget 就等于真实收费 | 不是。当前 `budget` 是执行上限和审计字段;真实收费需要 Runtime/Agent Manager 回传可核对 usage |
|
| Agent budget 就等于真实收费 | 不是。当前 `budget` 是执行上限和审计字段;真实收费需要 Runtime/Agent Manager 回传可核对 usage |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Heicode 桌面客户端 sub 敏捷流程 API 对接文档
|
# Heicode 桌面客户端 sub 敏捷流程 API 对接文档
|
||||||
|
|
||||||
更新时间:2026-05-28
|
更新时间:2026-05-30
|
||||||
适用范围:Heicode Desktop / 本地服务对接 Heicode Manager,跑通普通 sub 模式敏捷开发流程。
|
适用范围:Heicode Desktop / 本地服务对接 Heicode Manager,跑通普通 sub 模式敏捷开发流程。
|
||||||
Manager 生产地址:`https://code.xinghanlab.com`
|
Manager 生产地址:`https://code.xinghanlab.com`
|
||||||
|
|
||||||
@@ -145,12 +145,82 @@ Accept: application/json
|
|||||||
4. 当任务卡生成后,创建 deployment draft:POST /api/agnet/user/tasks/{task_id}/deployment-draft
|
4. 当任务卡生成后,创建 deployment draft:POST /api/agnet/user/tasks/{task_id}/deployment-draft
|
||||||
5. 创建 Manager deployment:POST /api/agnet/user/deployments
|
5. 创建 Manager deployment:POST /api/agnet/user/deployments
|
||||||
6. 轮询 deployment detail / events / timeline
|
6. 轮询 deployment detail / events / timeline
|
||||||
7. 展示 artifacts / sk-snapshots / logs / metrics
|
7. 查询 runtime-diagnostics,区分 callback 已到、Runtime 状态、失败 Agent 和兜底摘要 artifact
|
||||||
8. 如出现 approval,桌面端展示审批并调用 approve/reject
|
8. 查询 artifacts 列表;如果需要完整文件,调用 artifact content 代理接口下载
|
||||||
9. 完成后继续迭代或停止 deployment
|
9. 展示 sk-snapshots / logs / metrics
|
||||||
|
10. 如出现 approval,桌面端展示审批并调用 approve/reject
|
||||||
|
11. 完成后继续迭代或停止 deployment
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.1 2026-05-28 生产验证结果
|
### 3.1 Runtime 诊断接口
|
||||||
|
|
||||||
|
客户端展示普通 sub 结果时,不能只看 deployment `status=completed`。Manager 已提供只读诊断接口,用来识别 Runtime 是否真的完成、是否有 failed agent、是否只返回兜底摘要 artifact。
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/agnet/user/deployments/{deployment_id}/runtime-diagnostics
|
||||||
|
```
|
||||||
|
|
||||||
|
返回核心字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `runtime_mode` | `agnet` 表示普通 sub Runtime;`swarm` 表示蜂群 Runtime |
|
||||||
|
| `runtime_swarm_id` / `runtime_deployment_id` | Manager 保存的 Runtime 映射 |
|
||||||
|
| `data_source` | 当前诊断来源,正常为 `runtime_status` |
|
||||||
|
| `status` / `phase` | Runtime 直接返回的状态和阶段 |
|
||||||
|
| `agents` | Runtime 返回的 Agent 状态列表 |
|
||||||
|
| `artifacts` | Runtime status 里的产物摘要 |
|
||||||
|
| `metrics` | Runtime status 里的 usage/耗时等指标 |
|
||||||
|
| `warnings` | Manager 根据 Runtime status 识别出的异常 |
|
||||||
|
|
||||||
|
常见 `warnings`:
|
||||||
|
|
||||||
|
| warning | 客户端展示含义 |
|
||||||
|
|---|---|
|
||||||
|
| `runtime_agent_failed` | 存在失败 Agent,不能把任务说成完整交付 |
|
||||||
|
| `runtime_completed_with_failed_agents` | Runtime 总状态 completed,但内部 Agent 有失败,需要提示“执行异常完成” |
|
||||||
|
| `runtime_summary_artifact_only` | 只有 `Runtime execution summary` 兜底摘要,不是最终业务交付物 |
|
||||||
|
| `runtime_zero_model_usage` | Runtime 回传模型 token 为 0,说明 Agent 可能没有真实调用模型 |
|
||||||
|
| `runtime_status_query_failed` | Manager 无法查询 Runtime status,只能展示已落库 callback |
|
||||||
|
|
||||||
|
客户端建议:
|
||||||
|
|
||||||
|
1. deployment detail / timeline / artifacts 仍按原接口展示。
|
||||||
|
2. 若 `warnings` 包含 `runtime_summary_artifact_only`,需要提示“当前没有最终交付产物,请查看运行日志/等待 Runtime 修复”。
|
||||||
|
3. 若 `runtime_mode=agnet`,按普通 sub 敏捷展示;若 `runtime_mode=swarm`,按蜂群模式展示任务图/Agent 编队,不能混用两套文案。
|
||||||
|
4. 该接口只读,失败时不应中断已有 timeline/artifact 展示。
|
||||||
|
|
||||||
|
### 3.2 Artifact 完整内容下载
|
||||||
|
|
||||||
|
普通 sub 的 `artifact.created` callback 只保存摘要、URI、大小、hash 和 metadata。客户端或 Manager 页面需要完整产物正文时,先查列表,再走 Manager 用户态 content 代理接口,不要直接暴露 Blob 凭据、SAS URL 或 Runtime 内网地址。
|
||||||
|
|
||||||
|
列表:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/agnet/user/deployments/{deployment_id}/artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
下载:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/agnet/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
|
||||||
|
```
|
||||||
|
|
||||||
|
Manager 行为:
|
||||||
|
|
||||||
|
1. 校验当前用户拥有该 `deployment_id`。
|
||||||
|
2. 校验 `artifact_id` 属于该 deployment。
|
||||||
|
3. 使用 deployment 里保存的 `runtime_swarm_id` / `runtime_deployment_id` 请求 Runtime:
|
||||||
|
`GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content`。
|
||||||
|
4. 透传 Runtime 返回的文件内容、`Content-Type`、`Content-Disposition` 等安全响应头。
|
||||||
|
5. 如果 Runtime 未配置、artifact 不存在或 Runtime 返回错误,返回业务错误,不伪造内容。
|
||||||
|
|
||||||
|
页面说明:
|
||||||
|
|
||||||
|
- Manager Web 任务总览中的 artifact 卡片会显示“下载产物”。
|
||||||
|
- 如果 artifact 是 `Runtime execution summary` 兜底摘要,页面会明确提示它不是最终业务交付物。
|
||||||
|
|
||||||
|
### 3.3 2026-05-28 生产验证结果
|
||||||
|
|
||||||
本节记录已经按“桌面客户端应调用的顺序”在生产环境跑过的结果,客户端可按同一顺序和参数形状对接。
|
本节记录已经按“桌面客户端应调用的顺序”在生产环境跑过的结果,客户端可按同一顺序和参数形状对接。
|
||||||
|
|
||||||
@@ -215,7 +285,7 @@ Manager 登录
|
|||||||
- 如果客户端需要从自然语言创建任务,必须先完成 Heicode 登录并拿到 `heicode_access_token`。
|
- 如果客户端需要从自然语言创建任务,必须先完成 Heicode 登录并拿到 `heicode_access_token`。
|
||||||
- 当前 create / callback / detail / metrics / events / timeline / stop 已真实有效;artifacts / sk-snapshots 需要 Runtime 在真实任务中回写 `artifact.created` / `sk_tool.*` 后才会有数据。
|
- 当前 create / callback / detail / metrics / events / timeline / stop 已真实有效;artifacts / sk-snapshots 需要 Runtime 在真实任务中回写 `artifact.created` / `sk_tool.*` 后才会有数据。
|
||||||
|
|
||||||
### 3.2 桌面客户端联调结论
|
### 3.4 桌面客户端联调结论
|
||||||
|
|
||||||
普通 sub 模式可以开始桌面客户端联调。建议先按 Windows 最新客户端跑通,因为生产已有 Windows 设备绑定记录;macOS 端必须先确认客户端版本和 Keychain 凭据。
|
普通 sub 模式可以开始桌面客户端联调。建议先按 Windows 最新客户端跑通,因为生产已有 Windows 设备绑定记录;macOS 端必须先确认客户端版本和 Keychain 凭据。
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -768,6 +770,54 @@ func AgnetListUserDeploymentArtifacts(c *gin.Context) {
|
|||||||
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)})
|
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
|
||||||
|
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
artifactID := strings.TrimSpace(c.Param("artifact_id"))
|
||||||
|
if artifactID == "" {
|
||||||
|
agnetError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
artifact, found, err := model.GetAgnetArtifactByDeployment(record.DeploymentID, artifactID)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
|
||||||
|
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
agnetError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
|
||||||
|
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
|
||||||
|
agnetError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
resp, err := callAgnetRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
|
||||||
|
agnetError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
headers := map[string]string{}
|
||||||
|
for _, key := range []string{"Content-Disposition", "ETag", "Last-Modified", "Cache-Control"} {
|
||||||
|
if value := strings.TrimSpace(resp.Header.Get(key)); value != "" {
|
||||||
|
headers[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
c.DataFromReader(http.StatusOK, resp.ContentLength, contentType, resp.Body, headers)
|
||||||
|
}
|
||||||
|
|
||||||
func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
|
func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
|
||||||
var payload agnetCallbackEnvelope
|
var payload agnetCallbackEnvelope
|
||||||
if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil {
|
if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil {
|
||||||
|
|||||||
@@ -397,6 +397,135 @@ func TestAgnetRuntimeShadowCreateStoresRuntimeMapping(t *testing.T) {
|
|||||||
require.Equal(t, "accepted", stored.RuntimeState)
|
require.Equal(t, "accepted", stored.RuntimeState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgnetRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/status":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{
|
||||||
|
"deployment_id":"swarm-1",
|
||||||
|
"swarm_id":"swarm-1",
|
||||||
|
"status":"completed",
|
||||||
|
"phase":"development",
|
||||||
|
"agents":[{"agent_id":"agi_backend_1","role":"backend","status":"failed","output":"Cannot connect to host agent svc"}],
|
||||||
|
"artifacts":[{"artifact_id":"art_summary","artifact_type":"document","title":"Runtime execution summary","summary":"Runtime completed without per-agent artifacts; review swarm logs for details.","uri":"runtime://swarm-1/artifacts/summary"}],
|
||||||
|
"metrics":{"tokens_used":0}
|
||||||
|
}`))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
|
||||||
|
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
|
||||||
|
t.Setenv("AGNET_RUNTIME_BASE_URL", server.URL)
|
||||||
|
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "service-token")
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "7"
|
||||||
|
for idx := range plan.ResourceGrants {
|
||||||
|
plan.ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
for agentIdx := range plan.Agents {
|
||||||
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
||||||
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createRecorder, envelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.True(t, envelope.Success, createRecorder.Body.String())
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", 7)
|
||||||
|
ctx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/runtime-diagnostics", nil)
|
||||||
|
|
||||||
|
AgnetGetUserDeploymentRuntimeDiagnostics(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"data_source":"runtime_status"`)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"runtime_agent_failed"`)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"runtime_completed_with_failed_agents"`)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"runtime_summary_artifact_only"`)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"runtime_zero_model_usage"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetArtifactContentProxiesRuntimeContent(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "Bearer service-token", r.Header.Get("Authorization"))
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"success":true,"data":{"deployment_id":"runtime-dep-1","swarm_id":"swarm-1","status":"running"}}`))
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/api/swarms/swarm-1/artifacts/art-1/content":
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Disposition", `attachment; filename="art-1.txt"`)
|
||||||
|
_, _ = w.Write([]byte("artifact body"))
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
|
||||||
|
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
|
||||||
|
t.Setenv("AGNET_RUNTIME_BASE_URL", server.URL)
|
||||||
|
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "service-token")
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "7"
|
||||||
|
for idx := range plan.ResourceGrants {
|
||||||
|
plan.ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
for agentIdx := range plan.Agents {
|
||||||
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
||||||
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = "7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createRecorder, envelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.True(t, envelope.Success, createRecorder.Body.String())
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
require.NoError(t, model.UpsertAgnetArtifact(&model.AgnetArtifact{
|
||||||
|
ArtifactID: "art-1",
|
||||||
|
DeploymentID: deploymentID,
|
||||||
|
UserID: "7",
|
||||||
|
URI: "runtime://swarm-1/artifacts/art-1",
|
||||||
|
CreatedAtMs: time.Now().UnixMilli(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", 7)
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
{Key: "deployment_id", Value: deploymentID},
|
||||||
|
{Key: "artifact_id", Value: "art-1"},
|
||||||
|
}
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/artifacts/art-1/content", nil)
|
||||||
|
|
||||||
|
AgnetGetUserDeploymentArtifactContent(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.Equal(t, "artifact body", recorder.Body.String())
|
||||||
|
require.Equal(t, "text/plain; charset=utf-8", recorder.Header().Get("Content-Type"))
|
||||||
|
require.Equal(t, `attachment; filename="art-1.txt"`, recorder.Header().Get("Content-Disposition"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgnetRuntimeShadowCreateFailureDoesNotFailLocalDeployment(t *testing.T) {
|
func TestAgnetRuntimeShadowCreateFailureDoesNotFailLocalDeployment(t *testing.T) {
|
||||||
db := setupAgnetControlPlaneTestDB(t)
|
db := setupAgnetControlPlaneTestDB(t)
|
||||||
resetAgnetControlPlaneState(t)
|
resetAgnetControlPlaneState(t)
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type agnetRuntimeConfig struct {
|
|||||||
Token string
|
Token string
|
||||||
CreatePath string
|
CreatePath string
|
||||||
HealthPath string
|
HealthPath string
|
||||||
|
StatusPath string
|
||||||
|
ArtifactContentPath string
|
||||||
StopPath string
|
StopPath string
|
||||||
ApprovalDecisionPath string
|
ApprovalDecisionPath string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
@@ -44,6 +46,25 @@ type agnetRuntimeSyncResult struct {
|
|||||||
RawStatusCode int
|
RawStatusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type agnetRuntimeDiagnostics struct {
|
||||||
|
DeploymentID string `json:"deployment_id"`
|
||||||
|
RuntimeMode string `json:"runtime_mode"`
|
||||||
|
SubMode string `json:"sub_mode"`
|
||||||
|
RuntimeDeploymentID string `json:"runtime_deployment_id,omitempty"`
|
||||||
|
RuntimeSwarmID string `json:"runtime_swarm_id,omitempty"`
|
||||||
|
DataSource string `json:"data_source"`
|
||||||
|
HTTPStatus int `json:"http_status,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
Progress any `json:"progress,omitempty"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
Agents []gin.H `json:"agents"`
|
||||||
|
Artifacts []gin.H `json:"artifacts"`
|
||||||
|
Metrics map[string]any `json:"metrics,omitempty"`
|
||||||
|
Warnings []string `json:"warnings"`
|
||||||
|
CheckedAt string `json:"checked_at"`
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeAgnetRuntimeMode(value string) string {
|
func normalizeAgnetRuntimeMode(value string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case agnetRuntimeModeSwarm:
|
case agnetRuntimeModeSwarm:
|
||||||
@@ -101,6 +122,8 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
|
|||||||
Token: strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"SERVICE_TOKEN", "")),
|
Token: strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"SERVICE_TOKEN", "")),
|
||||||
CreatePath: common.GetEnvOrDefaultString(prefix+"CREATE_PATH", defaultCreatePath),
|
CreatePath: common.GetEnvOrDefaultString(prefix+"CREATE_PATH", defaultCreatePath),
|
||||||
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agnet/health"),
|
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agnet/health"),
|
||||||
|
StatusPath: common.GetEnvOrDefaultString(prefix+"STATUS_PATH", "/api/swarms/{swarm_id}/status"),
|
||||||
|
ArtifactContentPath: common.GetEnvOrDefaultString(prefix+"ARTIFACT_CONTENT_PATH", "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"),
|
||||||
StopPath: common.GetEnvOrDefaultString(prefix+"STOP_PATH", defaultStopPath),
|
StopPath: common.GetEnvOrDefaultString(prefix+"STOP_PATH", defaultStopPath),
|
||||||
ApprovalDecisionPath: common.GetEnvOrDefaultString(prefix+"APPROVAL_DECISION_PATH", defaultApprovalPath),
|
ApprovalDecisionPath: common.GetEnvOrDefaultString(prefix+"APPROVAL_DECISION_PATH", defaultApprovalPath),
|
||||||
Timeout: time.Duration(timeoutSec) * time.Second,
|
Timeout: time.Duration(timeoutSec) * time.Second,
|
||||||
@@ -505,6 +528,249 @@ func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymen
|
|||||||
return replacer.Replace(path)
|
return replacer.Replace(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
|
||||||
|
path := strings.TrimSpace(cfg.StatusPath)
|
||||||
|
if path == "" {
|
||||||
|
path = "/api/swarms/{swarm_id}/status"
|
||||||
|
}
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
|
||||||
|
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
|
||||||
|
"{deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
|
||||||
|
"{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
|
||||||
|
"{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)),
|
||||||
|
)
|
||||||
|
return replacer.Replace(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) string {
|
||||||
|
path := strings.TrimSpace(cfg.ArtifactContentPath)
|
||||||
|
if path == "" {
|
||||||
|
path = "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"
|
||||||
|
}
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
|
||||||
|
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
|
||||||
|
"{deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
|
||||||
|
"{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)),
|
||||||
|
"{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)),
|
||||||
|
"{artifact_id}", url.PathEscape(strings.TrimSpace(artifactID)),
|
||||||
|
)
|
||||||
|
return replacer.Replace(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord) (map[string]any, int, error) {
|
||||||
|
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
|
||||||
|
return nil, 0, errors.New("runtime identifiers missing")
|
||||||
|
}
|
||||||
|
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStatusPathForRecord(cfg, record))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
agnetRuntimeHeaders(req, cfg, record)
|
||||||
|
client := &http.Client{Timeout: cfg.Timeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, resp.StatusCode, readErr
|
||||||
|
}
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
|
return nil, resp.StatusCode, fmt.Errorf("runtime status returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
var envelope map[string]any
|
||||||
|
if len(body) > 0 {
|
||||||
|
if err := common.Unmarshal(body, &envelope); err != nil {
|
||||||
|
return nil, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
|
||||||
|
return nil, resp.StatusCode, errors.New(message)
|
||||||
|
}
|
||||||
|
return extractAgnetRuntimeData(envelope), resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) (*http.Response, error) {
|
||||||
|
if strings.TrimSpace(artifactID) == "" {
|
||||||
|
return nil, errors.New("artifact_id is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
|
||||||
|
return nil, errors.New("runtime identifiers missing")
|
||||||
|
}
|
||||||
|
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
agnetRuntimeHeaders(req, cfg, record)
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
client := &http.Client{Timeout: cfg.Timeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
return nil, fmt.Errorf("runtime artifact content returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapSliceFromAny(value any) []gin.H {
|
||||||
|
raw, ok := value.([]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
items := make([]gin.H, 0, len(raw))
|
||||||
|
for _, entry := range raw {
|
||||||
|
if item, ok := entry.(map[string]any); ok {
|
||||||
|
items = append(items, gin.H(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapFromAny(value any) map[string]any {
|
||||||
|
if item, ok := value.(map[string]any); ok {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeAgentHasFailed(agents []gin.H) bool {
|
||||||
|
for _, agent := range agents {
|
||||||
|
status := strings.ToLower(strings.TrimSpace(fmt.Sprint(agent["status"])))
|
||||||
|
if strings.Contains(status, "fail") ||
|
||||||
|
strings.Contains(status, "crash") ||
|
||||||
|
strings.Contains(status, "error") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func runtimeArtifactsAreSummaryOnly(artifacts []gin.H) bool {
|
||||||
|
if len(artifacts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, artifact := range artifacts {
|
||||||
|
title := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["title"])))
|
||||||
|
summary := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["summary"])))
|
||||||
|
uri := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["uri"])))
|
||||||
|
if strings.Contains(summary, "without per-agent artifacts") ||
|
||||||
|
strings.Contains(title, "runtime execution summary") ||
|
||||||
|
strings.Contains(uri, "/artifacts/summary") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]any, httpStatus int, source string) agnetRuntimeDiagnostics {
|
||||||
|
agents := mapSliceFromAny(data["agents"])
|
||||||
|
artifacts := mapSliceFromAny(data["artifacts"])
|
||||||
|
status := stringFromMap(data, "runtime_status", "status")
|
||||||
|
metrics := mapFromAny(data["metrics"])
|
||||||
|
warnings := make([]string, 0, 4)
|
||||||
|
agentFailed := runtimeAgentHasFailed(agents)
|
||||||
|
if agentFailed {
|
||||||
|
warnings = append(warnings, "runtime_agent_failed")
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(status), "completed") && agentFailed {
|
||||||
|
warnings = append(warnings, "runtime_completed_with_failed_agents")
|
||||||
|
}
|
||||||
|
if runtimeArtifactsAreSummaryOnly(artifacts) {
|
||||||
|
warnings = append(warnings, "runtime_summary_artifact_only")
|
||||||
|
}
|
||||||
|
if metrics != nil {
|
||||||
|
tokens := fmt.Sprint(metrics["tokens_used"])
|
||||||
|
if tokens == "0" || tokens == "0.0" {
|
||||||
|
warnings = append(warnings, "runtime_zero_model_usage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return agnetRuntimeDiagnostics{
|
||||||
|
DeploymentID: record.DeploymentID,
|
||||||
|
RuntimeMode: agnetRuntimeModeForRecord(record),
|
||||||
|
SubMode: record.SubMode,
|
||||||
|
RuntimeDeploymentID: record.RuntimeDeploymentID,
|
||||||
|
RuntimeSwarmID: record.RuntimeSwarmID,
|
||||||
|
DataSource: source,
|
||||||
|
HTTPStatus: httpStatus,
|
||||||
|
Status: status,
|
||||||
|
Phase: stringFromMap(data, "phase", "stage"),
|
||||||
|
Progress: data["progress"],
|
||||||
|
ErrorMessage: stringFromMap(data, "error_message", "failure_reason", "error"),
|
||||||
|
Agents: agents,
|
||||||
|
Artifacts: artifacts,
|
||||||
|
Metrics: metrics,
|
||||||
|
Warnings: warnings,
|
||||||
|
CheckedAt: agnetNow(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymentRecord) agnetRuntimeDiagnostics {
|
||||||
|
mode := agnetRuntimeModeForRecord(record)
|
||||||
|
cfg := agnetRuntimeClientConfigForMode(mode)
|
||||||
|
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
|
||||||
|
return agnetRuntimeDiagnostics{
|
||||||
|
DeploymentID: record.DeploymentID,
|
||||||
|
RuntimeMode: mode,
|
||||||
|
SubMode: record.SubMode,
|
||||||
|
DataSource: "not_configured",
|
||||||
|
Warnings: []string{"runtime_not_configured"},
|
||||||
|
CheckedAt: agnetNow(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
|
||||||
|
return agnetRuntimeDiagnostics{
|
||||||
|
DeploymentID: record.DeploymentID,
|
||||||
|
RuntimeMode: mode,
|
||||||
|
SubMode: record.SubMode,
|
||||||
|
DataSource: "missing_runtime_id",
|
||||||
|
Warnings: []string{"runtime_identifiers_missing"},
|
||||||
|
CheckedAt: agnetNow(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, status, err := callAgnetRuntimeStatus(ctx, cfg, record)
|
||||||
|
if err != nil {
|
||||||
|
return agnetRuntimeDiagnostics{
|
||||||
|
DeploymentID: record.DeploymentID,
|
||||||
|
RuntimeMode: mode,
|
||||||
|
SubMode: record.SubMode,
|
||||||
|
RuntimeDeploymentID: record.RuntimeDeploymentID,
|
||||||
|
RuntimeSwarmID: record.RuntimeSwarmID,
|
||||||
|
DataSource: "runtime_status_error",
|
||||||
|
HTTPStatus: status,
|
||||||
|
ErrorMessage: truncateAgnetFailureReason(err.Error()),
|
||||||
|
Warnings: []string{"runtime_status_query_failed"},
|
||||||
|
CheckedAt: agnetNow(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildAgnetRuntimeDiagnostics(record, data, status, "runtime_status")
|
||||||
|
}
|
||||||
|
|
||||||
|
func AgnetGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
|
||||||
|
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record)).Timeout)
|
||||||
|
defer cancel()
|
||||||
|
common.ApiSuccess(c, agnetRuntimeDiagnosticsForRecord(ctx, record))
|
||||||
|
}
|
||||||
|
|
||||||
func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string {
|
func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string {
|
||||||
path := strings.TrimSpace(cfg.ApprovalDecisionPath)
|
path := strings.TrimSpace(cfg.ApprovalDecisionPath)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type AgnetArtifact struct {
|
type AgnetArtifact struct {
|
||||||
Id int `gorm:"primaryKey" json:"id"`
|
Id int `gorm:"primaryKey" json:"id"`
|
||||||
ArtifactID string `gorm:"type:varchar(128);uniqueIndex" json:"artifact_id"`
|
ArtifactID string `gorm:"type:varchar(128);uniqueIndex" json:"artifact_id"`
|
||||||
@@ -64,3 +71,23 @@ func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
|
|||||||
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
|
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
|
||||||
return items, err
|
return items, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAgnetArtifactByDeployment(deploymentID string, artifactID string) (AgnetArtifact, bool, error) {
|
||||||
|
var row AgnetArtifact
|
||||||
|
if DB == nil {
|
||||||
|
return row, false, nil
|
||||||
|
}
|
||||||
|
deploymentID = strings.TrimSpace(deploymentID)
|
||||||
|
artifactID = strings.TrimSpace(artifactID)
|
||||||
|
if deploymentID == "" || artifactID == "" {
|
||||||
|
return row, false, nil
|
||||||
|
}
|
||||||
|
err := DB.Where("deployment_id = ? AND artifact_id = ?", deploymentID, artifactID).First(&row).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return row, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return row, false, err
|
||||||
|
}
|
||||||
|
return row, true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -509,9 +509,11 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
agnetApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgnetStopUserDeployment)
|
agnetApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgnetStopUserDeployment)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgnetListUserDeploymentLogs)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgnetListUserDeploymentLogs)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgnetGetUserDeploymentMetrics)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgnetGetUserDeploymentMetrics)
|
||||||
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgnetGetUserDeploymentRuntimeDiagnostics)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgnetListUserDeploymentEvents)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgnetListUserDeploymentEvents)
|
||||||
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
|
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgnetListUserDeploymentArtifacts)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgnetListUserDeploymentArtifacts)
|
||||||
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgnetGetUserDeploymentArtifactContent)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgnetListUserSKSnapshots)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgnetListUserSKSnapshots)
|
||||||
agnetApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgnetGetUserDeploymentTimeline)
|
agnetApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgnetGetUserDeploymentTimeline)
|
||||||
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
|
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
|
||||||
|
|||||||
Vendored
+21
-3
@@ -1,9 +1,27 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Heicode Manager placeholder</title>
|
<link rel="icon" type="image/svg+xml" href="/heicode-logo.svg?v=h-glass-2" />
|
||||||
</head>
|
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=h-glass-2" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico?v=h-glass-2" />
|
||||||
|
<link rel="apple-touch-icon" href="/logo.png?v=h-glass-2" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>Heicode Manager</title>
|
||||||
|
<meta name="title" content="Heicode Manager" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
|
||||||
|
/>
|
||||||
|
<meta property="og:title" content="Heicode Manager" />
|
||||||
|
<meta property="og:image" content="/logo.png?v=h-glass-2" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
|
||||||
|
<meta name="theme-color" content="#7B6BE3" />
|
||||||
|
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.8fa3e0a349.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/9238.45c9c35ccf.js"></script><script defer src="/static/js/index.c2989bbeef.js"></script><link href="/static/css/index.fd51d44fe8.css" rel="stylesheet"></head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -179,6 +179,25 @@ export type AgnetDeployment = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AgnetRuntimeDiagnostics = {
|
||||||
|
deployment_id: string
|
||||||
|
runtime_mode?: 'agnet' | 'swarm' | string
|
||||||
|
sub_mode?: AgnetSubMode
|
||||||
|
runtime_deployment_id?: string
|
||||||
|
runtime_swarm_id?: string
|
||||||
|
data_source?: string
|
||||||
|
http_status?: number
|
||||||
|
status?: string
|
||||||
|
phase?: string
|
||||||
|
progress?: unknown
|
||||||
|
error_message?: string
|
||||||
|
agents?: Array<Record<string, unknown>>
|
||||||
|
artifacts?: Array<Record<string, unknown>>
|
||||||
|
metrics?: Record<string, unknown>
|
||||||
|
warnings?: string[]
|
||||||
|
checked_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AgnetApprovalRequest = {
|
export type AgnetApprovalRequest = {
|
||||||
approval_id: string
|
approval_id: string
|
||||||
user_id: number
|
user_id: number
|
||||||
@@ -360,6 +379,20 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAgnetRuntimeDiagnostics(
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<AgnetRuntimeDiagnostics | null> {
|
||||||
|
const res = await api.get<ApiEnvelope<AgnetRuntimeDiagnostics>>(
|
||||||
|
`/api/agnet/user/deployments/${deploymentId}/runtime-diagnostics`,
|
||||||
|
{
|
||||||
|
skipBusinessError: true,
|
||||||
|
skipErrorHandler: true,
|
||||||
|
} as Record<string, unknown>
|
||||||
|
)
|
||||||
|
if (!res.data?.success) return null
|
||||||
|
return res.data?.data ?? null
|
||||||
|
}
|
||||||
|
|
||||||
export async function simulateAgnetDeploymentEvents(
|
export async function simulateAgnetDeploymentEvents(
|
||||||
deploymentId: string,
|
deploymentId: string,
|
||||||
events?: string[]
|
events?: string[]
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import { QueryState } from '@/components/query-state'
|
|||||||
import {
|
import {
|
||||||
approveAgnetApproval,
|
approveAgnetApproval,
|
||||||
getAgnetDeploymentEvents,
|
getAgnetDeploymentEvents,
|
||||||
|
getAgnetRuntimeDiagnostics,
|
||||||
getAgnetDeploymentTimeline,
|
getAgnetDeploymentTimeline,
|
||||||
listAgnetApprovals,
|
listAgnetApprovals,
|
||||||
listAgnetCredentialLeases,
|
listAgnetCredentialLeases,
|
||||||
@@ -73,6 +74,7 @@ import {
|
|||||||
type AgnetApprovalRequest,
|
type AgnetApprovalRequest,
|
||||||
type AgnetCredentialLease,
|
type AgnetCredentialLease,
|
||||||
type AgnetDeployment,
|
type AgnetDeployment,
|
||||||
|
type AgnetRuntimeDiagnostics,
|
||||||
type AgnetRuntimeExecution,
|
type AgnetRuntimeExecution,
|
||||||
type AgnetSKAccessPolicy,
|
type AgnetSKAccessPolicy,
|
||||||
} from './api'
|
} from './api'
|
||||||
@@ -354,6 +356,17 @@ function artifactTypeToneClass(value: unknown): string {
|
|||||||
return 'bg-muted/40 text-muted-foreground ring-border/60'
|
return 'bg-muted/40 text-muted-foreground ring-border/60'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFallbackRuntimeArtifact(item: Record<string, unknown>): boolean {
|
||||||
|
const title = String(item.title || '').toLowerCase()
|
||||||
|
const summary = String(item.summary || '').toLowerCase()
|
||||||
|
const uri = String(item.uri || '').toLowerCase()
|
||||||
|
return (
|
||||||
|
title.includes('runtime execution summary') ||
|
||||||
|
summary.includes('without per-agent artifacts') ||
|
||||||
|
uri.includes('/artifacts/summary')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function isTaskFlowEvent(eventType: unknown): boolean {
|
function isTaskFlowEvent(eventType: unknown): boolean {
|
||||||
const event = String(eventType || '').toLowerCase()
|
const event = String(eventType || '').toLowerCase()
|
||||||
return event.startsWith('task.') || event.startsWith('handoff.')
|
return event.startsWith('task.') || event.startsWith('handoff.')
|
||||||
@@ -530,6 +543,145 @@ function formatGrantStatus(
|
|||||||
return status || t('Active')
|
return status || t('Active')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runtimeWarningLabel(value: string, t: (key: string) => string): string {
|
||||||
|
switch (value) {
|
||||||
|
case 'runtime_agent_failed':
|
||||||
|
return t('Runtime agent failed')
|
||||||
|
case 'runtime_completed_with_failed_agents':
|
||||||
|
return t('Runtime completed with failed agents')
|
||||||
|
case 'runtime_summary_artifact_only':
|
||||||
|
return t('Only fallback summary artifact returned')
|
||||||
|
case 'runtime_zero_model_usage':
|
||||||
|
return t('Runtime reported zero model usage')
|
||||||
|
case 'runtime_status_query_failed':
|
||||||
|
return t('Runtime status query failed')
|
||||||
|
case 'runtime_not_configured':
|
||||||
|
return t('Runtime not configured')
|
||||||
|
case 'runtime_identifiers_missing':
|
||||||
|
return t('Runtime identifiers missing')
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeModeLabel(
|
||||||
|
diagnostics: AgnetRuntimeDiagnostics | null | undefined,
|
||||||
|
t: (key: string) => string
|
||||||
|
): string {
|
||||||
|
const mode = String(diagnostics?.runtime_mode || '').toLowerCase()
|
||||||
|
if (mode === 'swarm') return t('Swarm mode')
|
||||||
|
if (mode === 'agnet') return t('Ordinary sub mode')
|
||||||
|
return mode || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeAgentRows(
|
||||||
|
diagnostics: AgnetRuntimeDiagnostics | null | undefined
|
||||||
|
) {
|
||||||
|
return (diagnostics?.agents ?? []).slice(0, 4).map((agent, idx) => ({
|
||||||
|
id: String(agent.agent_id || agent.instance_id || idx),
|
||||||
|
role: String(agent.role || '—'),
|
||||||
|
status: String(agent.status || agent.runtime_state || '—'),
|
||||||
|
output: String(agent.output || agent.failure_reason || ''),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuntimeDiagnosticsPanel({
|
||||||
|
diagnostics,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
diagnostics?: AgnetRuntimeDiagnostics | null
|
||||||
|
isLoading: boolean
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const warnings = diagnostics?.warnings ?? []
|
||||||
|
const agents = runtimeAgentRows(diagnostics)
|
||||||
|
const hasWarning = warnings.length > 0
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mt-3 rounded-xl border p-3',
|
||||||
|
hasWarning
|
||||||
|
? 'border-amber-500/35 bg-amber-500/10'
|
||||||
|
: 'border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/35'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='flex items-start justify-between gap-3'>
|
||||||
|
<div>
|
||||||
|
<p className='text-foreground flex items-center gap-2 text-xs font-semibold'>
|
||||||
|
<AlertOctagon
|
||||||
|
className={cn(
|
||||||
|
'h-3.5 w-3.5',
|
||||||
|
hasWarning ? 'text-amber-400' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{t('Runtime diagnostics')}
|
||||||
|
</p>
|
||||||
|
<p className='text-muted-foreground mt-1 text-xs'>
|
||||||
|
{t(
|
||||||
|
'Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
|
<CircleDashed className='text-muted-foreground h-4 w-4 animate-spin' />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-3 grid gap-2 sm:grid-cols-3'>
|
||||||
|
<MetaPill
|
||||||
|
icon={Rocket}
|
||||||
|
label={t('runtime mode')}
|
||||||
|
value={runtimeModeLabel(diagnostics, t)}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={Activity}
|
||||||
|
label={t('runtime status')}
|
||||||
|
value={formatStatusLabel(diagnostics?.status || '—', t)}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={FileSearch}
|
||||||
|
label={t('artifact source')}
|
||||||
|
value={String(diagnostics?.data_source || '—')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasWarning && (
|
||||||
|
<div className='mt-3 flex flex-wrap gap-1.5'>
|
||||||
|
{warnings.map((warning) => (
|
||||||
|
<span
|
||||||
|
key={warning}
|
||||||
|
className='rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-200 ring-1 ring-amber-500/30 ring-inset'
|
||||||
|
>
|
||||||
|
{runtimeWarningLabel(warning, t)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agents.length > 0 && (
|
||||||
|
<div className='mt-3 space-y-1.5'>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<div
|
||||||
|
key={agent.id}
|
||||||
|
className='bg-background/45 rounded-lg px-2 py-1.5 text-[11px]'
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-2'>
|
||||||
|
<span className='font-mono'>{agent.role}</span>
|
||||||
|
<StatusBadge phase={agent.status} />
|
||||||
|
</div>
|
||||||
|
{agent.output && (
|
||||||
|
<p className='text-muted-foreground mt-1 line-clamp-2'>
|
||||||
|
{agent.output}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -538,6 +690,13 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
|||||||
const phase = dep.phase || dep.status
|
const phase = dep.phase || dep.status
|
||||||
const risk = describeRiskLevel(dep)
|
const risk = describeRiskLevel(dep)
|
||||||
const grants = collectResourceGrants(dep)
|
const grants = collectResourceGrants(dep)
|
||||||
|
const runtimeDiagnosticsQuery = useQuery({
|
||||||
|
queryKey: ['agnet', 'runtime-diagnostics', dep.deployment_id],
|
||||||
|
queryFn: () => getAgnetRuntimeDiagnostics(dep.deployment_id),
|
||||||
|
enabled: Boolean(dep.deployment_id),
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
const runtimeDiagnostics = runtimeDiagnosticsQuery.data
|
||||||
const simulateMutation = useMutation({
|
const simulateMutation = useMutation({
|
||||||
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
|
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -650,6 +809,11 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RuntimeDiagnosticsPanel
|
||||||
|
diagnostics={runtimeDiagnostics}
|
||||||
|
isLoading={runtimeDiagnosticsQuery.isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-3 md:grid-cols-3'>
|
<div className='mt-4 grid gap-3 md:grid-cols-3'>
|
||||||
<div className='bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3 md:col-span-3'>
|
<div className='bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3 md:col-span-3'>
|
||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
@@ -1309,11 +1473,29 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
<p className='text-muted-foreground mt-1 line-clamp-2 text-[11px]'>
|
<p className='text-muted-foreground mt-1 line-clamp-2 text-[11px]'>
|
||||||
{String(item.summary || item.uri || '—')}
|
{String(item.summary || item.uri || '—')}
|
||||||
</p>
|
</p>
|
||||||
|
{isFallbackRuntimeArtifact(item) && (
|
||||||
|
<p className='mt-1 rounded-md bg-amber-500/10 px-2 py-1 text-[10px] text-amber-200 ring-1 ring-amber-500/25 ring-inset'>
|
||||||
|
{t(
|
||||||
|
'Fallback summary only; not a final business deliverable.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{Boolean(item.uri) && (
|
{Boolean(item.uri) && (
|
||||||
<p className='text-muted-foreground mt-1 truncate font-mono text-[10px]'>
|
<p className='text-muted-foreground mt-1 truncate font-mono text-[10px]'>
|
||||||
{String(item.uri)}
|
{String(item.uri)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{Boolean(item.artifact_id) && (
|
||||||
|
<a
|
||||||
|
href={`/api/agnet/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className='text-primary mt-2 inline-flex items-center gap-1 text-[11px] font-medium hover:underline'
|
||||||
|
>
|
||||||
|
<ArrowUpRight className='h-3 w-3' />
|
||||||
|
{t('Download artifact')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+18
@@ -3478,6 +3478,24 @@
|
|||||||
"Task not found. It may have been removed or was never created.": "Task not found. It may have been removed or was never created.",
|
"Task not found. It may have been removed or was never created.": "Task not found. It may have been removed or was never created.",
|
||||||
"Task overview": "Task overview",
|
"Task overview": "Task overview",
|
||||||
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
|
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
|
||||||
|
"Runtime diagnostics": "Runtime diagnostics",
|
||||||
|
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.",
|
||||||
|
"runtime mode": "runtime mode",
|
||||||
|
"runtime status": "runtime status",
|
||||||
|
"artifact source": "artifact source",
|
||||||
|
"Runtime agent failed": "Runtime agent failed",
|
||||||
|
"Runtime completed with failed agents": "Runtime completed with failed agents",
|
||||||
|
"Only fallback summary artifact returned": "Only fallback summary artifact returned",
|
||||||
|
"Runtime reported zero model usage": "Runtime reported zero model usage",
|
||||||
|
"Runtime status query failed": "Runtime status query failed",
|
||||||
|
"Runtime not configured": "Runtime not configured",
|
||||||
|
"Runtime identifiers missing": "Runtime identifiers missing",
|
||||||
|
"Swarm mode": "Swarm mode",
|
||||||
|
"Ordinary sub mode": "Ordinary sub mode",
|
||||||
|
"Artifacts": "Artifacts",
|
||||||
|
"No artifacts yet": "No artifacts yet",
|
||||||
|
"Fallback summary only; not a final business deliverable.": "Fallback summary only; not a final business deliverable.",
|
||||||
|
"Download artifact": "Download artifact",
|
||||||
"Team Collaboration": "Team Collaboration",
|
"Team Collaboration": "Team Collaboration",
|
||||||
"Technical Support": "Technical Support",
|
"Technical Support": "Technical Support",
|
||||||
"Telegram": "Telegram",
|
"Telegram": "Telegram",
|
||||||
|
|||||||
+16
@@ -3482,6 +3482,20 @@
|
|||||||
"Task not found. It may have been removed or was never created.": "任务不存在,可能已被删除或从未创建。",
|
"Task not found. It may have been removed or was never created.": "任务不存在,可能已被删除或从未创建。",
|
||||||
"Task overview": "任务总览",
|
"Task overview": "任务总览",
|
||||||
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "在客户端确认推荐摘要并启动 Agnet 后,任务会出现在这里。",
|
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "在客户端确认推荐摘要并启动 Agnet 后,任务会出现在这里。",
|
||||||
|
"Runtime diagnostics": "运行时诊断",
|
||||||
|
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager 会独立检查运行时状态,并明确区分普通 sub 与蜂群模式,不混用回调数据。",
|
||||||
|
"runtime mode": "运行模式",
|
||||||
|
"runtime status": "运行状态",
|
||||||
|
"artifact source": "产物来源",
|
||||||
|
"Runtime agent failed": "运行时智能体失败",
|
||||||
|
"Runtime completed with failed agents": "运行时已结束但存在失败智能体",
|
||||||
|
"Only fallback summary artifact returned": "只返回了兜底摘要产物",
|
||||||
|
"Runtime reported zero model usage": "运行时模型用量为 0",
|
||||||
|
"Runtime status query failed": "运行时状态查询失败",
|
||||||
|
"Runtime not configured": "运行时未配置",
|
||||||
|
"Runtime identifiers missing": "缺少运行时标识",
|
||||||
|
"Swarm mode": "蜂群模式",
|
||||||
|
"Ordinary sub mode": "普通 sub 模式",
|
||||||
"Team Collaboration": "团队协作",
|
"Team Collaboration": "团队协作",
|
||||||
"Technical Support": "技术支持",
|
"Technical Support": "技术支持",
|
||||||
"Telegram": "Telegram",
|
"Telegram": "Telegram",
|
||||||
@@ -4148,6 +4162,8 @@
|
|||||||
"Agent role": "智能体角色",
|
"Agent role": "智能体角色",
|
||||||
"Artifacts": "产物",
|
"Artifacts": "产物",
|
||||||
"No artifacts yet": "暂无产物",
|
"No artifacts yet": "暂无产物",
|
||||||
|
"Fallback summary only; not a final business deliverable.": "仅为运行时兜底摘要,不是最终业务交付物。",
|
||||||
|
"Download artifact": "下载产物",
|
||||||
"SK snapshots": "SK 快照",
|
"SK snapshots": "SK 快照",
|
||||||
"No SK snapshots yet": "暂无 SK 快照",
|
"No SK snapshots yet": "暂无 SK 快照",
|
||||||
"Merged timeline": "合并时间线",
|
"Merged timeline": "合并时间线",
|
||||||
|
|||||||
Reference in New Issue
Block a user