feat: document and expose swarm runtime contract
This commit is contained in:
@@ -70,16 +70,16 @@ Heicode 桌面客户端
|
|||||||
|
|
||||||
| 优先级 | 任务 | 当前缺口 | 是否 Manager 可独立做 | 验收标准 |
|
| 优先级 | 任务 | 当前缺口 | 是否 Manager 可独立做 | 验收标准 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| P0 | 把 `/api/swarms` adapter 文档化并固定字段 | 代码已有入口,但文档要明确它和 Runtime `POST /api/swarms` / `/api/agnet/deployments` 的映射 | 是 | 文档写清 request/response、source、`deployment_id <-> swarm_id` 映射 |
|
| P0 | 把 `/api/swarms` adapter 文档化并固定字段 | 已完成:`docs/integration/蜂群模式-AgentManager对接任务清单.md` 和 `docs/integration/普通sub敏捷模式-AgentManager对接任务清单.md` 已写清 Runtime create、callback、source、`deployment_id <-> swarm_id` 映射 | 是 | 文档可直接发给 Agent Manager / 蜂群侧核对 |
|
||||||
| P0 | 增加 Swarm Run 显示字段 | 页面需要明确展示 `runtime_deployment_id`、`runtime_swarm_id`、`runtime_state`、source | 是 | 用户态 deployment 详情和后台页面能看到 Runtime 映射 |
|
| P0 | 增加 Swarm Run 显示字段 | 已完成:用户态 deployment 详情展示 `runtime_deployment_id`、`runtime_swarm_id`、`runtime_state`,并在相关记录里展示 source | 是 | 用户态 deployment 详情和后台页面能看到 Runtime 映射 |
|
||||||
| P0 | 回调事件类型收敛 | 目前统一 `swarm-events` 已能接,但需要按蜂群事件标准列出必填字段和展示规则 | 是 | `task.created/claimed/running/completed/failed/handoff/approval/artifact` 都有 schema |
|
| P0 | 回调事件类型收敛 | 已完成:`GET /api/agnet/callbacks/swarm-events/schema` 输出事件类型、分类和必填字段;callback 接收端按 schema 校验关键 task/handoff/artifact/approval 字段 | 是 | `task.created/claimed/running/completed/failed/handoff/approval/artifact` 都有 schema |
|
||||||
| P0 | Runtime 联调配置模板 | 需要把 `AGNET_RUNTIME_*` 配置、token、callback URL、签名方式整理给蜂群侧 | 是 | 蜂群项目按模板能调用 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 展示优化 | 已能在页面展示 artifact 类型和 URI;仍需等 Runtime 输出真实 code_patch/document/test_report/deployment_manifest | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 |
|
| P1 | Artifact 展示优化 | Manager 端已完成:页面展示 artifact 类型、摘要和 URI;真实 `code_patch/document/test_report/deployment_manifest` 仍需 Runtime 输出 | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 |
|
||||||
| P1 | 任务图/Agent 状态展示占位 | Manager 还没有真实 task graph/claim/heartbeat 页面结构 | Manager 可先做展示结构,真实数据需 Runtime | 有空态和字段,不宣称真实已运行 |
|
| P1 | 任务图/Agent 状态展示占位 | 已完成:页面从 `task.*` / `handoff.*` callback 聚合 Agent task map;无真实数据时显示 Runtime callback 空态 | Manager 可先做展示结构,真实数据需 Runtime | 有空态和字段,不宣称真实已运行 |
|
||||||
| P1 | 日志/指标真实来源标识 | deployment 详情、timeline 已强化 runtime/simulated 来源;logs/metrics 仍需 Runtime 真实数据 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 |
|
| P1 | 日志/指标真实来源标识 | 已完成:logs/metrics API 返回 `data_source`、`runtime_source`,当前明确是 Manager control-plane / estimated,不伪装 Runtime 真实指标 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 |
|
||||||
| P1 | 高危审批客户端联动文档 | Manager 有 API,桌面客户端需要知道如何轮询/展示/提交审批 | 是 | 客户端文档补齐 approval flow |
|
| P1 | 高危审批客户端联动文档 | 已完成:`docs/integration/heicode-desktop-sub-agile-api.md` 已包含 approval 查询、approve/reject、awaiting_approval 流程 | 是 | 客户端文档补齐 approval flow |
|
||||||
| P2 | 蜂群模式验收脚本 | Manager 已有默认模拟链路和定向单测;还需要整理生产 curl 脚本 | 是 | 本地/生产能跑出 callback、artifact、approval、timeline 可见 |
|
| P2 | 蜂群模式验收脚本 | 已完成:`scripts/agnet_sub_mode_smoke.py` 支持 schema 检查、生产健康检查、可选 simulate-events、可选真实 callback smoke | 是 | 本地/生产能跑出 callback、artifact、approval、timeline 可见 |
|
||||||
|
|
||||||
## 五、需要蜂群项目配合的事项
|
## 五、需要蜂群项目配合的事项
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,624 @@
|
|||||||
|
# Agent Manager 蜂群 Runtime 接口实现要求
|
||||||
|
|
||||||
|
更新时间:2026-05-28
|
||||||
|
面向对象:Agent Manager / HeiCode-Swarm Runtime 开发负责人
|
||||||
|
用途:Agent Manager 按本文实现接口、字段和回调后,Heicode Manager 可直接联调蜂群模式。
|
||||||
|
|
||||||
|
## 1. 结论
|
||||||
|
|
||||||
|
Heicode Manager 已负责控制面和记录面:
|
||||||
|
|
||||||
|
- 生成 deployment / swarm 创建请求。
|
||||||
|
- 传递 `resource_grants`、`secret_ref`、预算、模型网关上下文和 callback 地址。
|
||||||
|
- 保存 `deployment_id <-> runtime_deployment_id <-> swarm_id` 映射。
|
||||||
|
- 接收 Runtime callback,落库 artifact / approval / timeline / audit。
|
||||||
|
- 用户 approve/reject 后,把审批决定回传 Runtime。
|
||||||
|
|
||||||
|
Agent Manager / Swarm Runtime 需要负责执行层:
|
||||||
|
|
||||||
|
- 创建真实 Swarm Run。
|
||||||
|
- 生成真实 task graph。
|
||||||
|
- 管理 Agent claim / heartbeat / handoff / retry / blocked / failed 状态机。
|
||||||
|
- 真实执行任务并产出 artifact。
|
||||||
|
- 在高危动作前暂停并回调审批请求。
|
||||||
|
- 接收审批结果后继续或停止。
|
||||||
|
- 回传日志、指标、用量和最终结果。
|
||||||
|
|
||||||
|
## 2. 总体调用链
|
||||||
|
|
||||||
|
```text
|
||||||
|
Heicode Desktop Client
|
||||||
|
-> Heicode Manager
|
||||||
|
POST /api/swarms 或 /api/agnet/user/deployments
|
||||||
|
-> Agent Manager / Swarm Runtime
|
||||||
|
POST /api/swarms
|
||||||
|
<- Runtime response
|
||||||
|
runtime_deployment_id / swarm_id / status
|
||||||
|
<- Runtime callback
|
||||||
|
POST /api/agnet/callbacks/swarm-events
|
||||||
|
-> Runtime approval decision
|
||||||
|
POST /api/swarms/{swarm_id}/approvals/{approval_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Agent Manager 必须提供的接口
|
||||||
|
|
||||||
|
| 优先级 | 方法 | 路径 | 必须 | 用途 |
|
||||||
|
|---|---|---|---:|---|
|
||||||
|
| P0 | `GET` | `/api/agnet/health` | 是 | 健康检查 |
|
||||||
|
| P0 | `POST` | `/api/swarms` | 是 | 创建真实 Swarm Run |
|
||||||
|
| P0 | `POST` | `/api/swarms/{swarm_id}/stop` | 是 | 停止 Swarm Run |
|
||||||
|
| P0 | `POST` | `/api/swarms/{swarm_id}/approvals/{approval_id}` | 是 | 接收 Manager 审批决定 |
|
||||||
|
| P0 | callback | Manager `/api/agnet/callbacks/swarm-events` | 是 | 回写状态、task、handoff、artifact、approval |
|
||||||
|
| P1 | `GET` | `/api/swarms/{swarm_id}` | 建议 | 查询 Runtime 详情 |
|
||||||
|
| P1 | `GET` | `/api/swarms/{swarm_id}/tasks` | 建议 | 查询 task graph |
|
||||||
|
| P1 | `GET` | `/api/swarms/{swarm_id}/logs` | 建议 | 查询日志 |
|
||||||
|
| P1 | `GET` | `/api/swarms/{swarm_id}/metrics` | 建议 | 查询指标 |
|
||||||
|
|
||||||
|
如果短期无法提供 `/api/swarms`,可以临时确认兼容路径,例如 `/tasks`。但这只能作为联调过渡,不作为最终生产契约。
|
||||||
|
|
||||||
|
## 4. 认证与公共 Header
|
||||||
|
|
||||||
|
Manager 调 Agent Manager 时携带:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <service_token>
|
||||||
|
X-User-ID: <manager_user_id>
|
||||||
|
X-Binding-Scope: <binding_scope>
|
||||||
|
X-Correlation-ID: <correlation_id>
|
||||||
|
X-Idempotency-Key: manager-<manager_deployment_id>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. `Authorization` 用于服务间鉴权。
|
||||||
|
2. `X-Correlation-ID` 必须贯穿 create、callback、approval decision、logs、metrics。
|
||||||
|
3. `X-Idempotency-Key` 必须支持幂等;重复创建请求不能生成多个真实 Swarm Run。
|
||||||
|
4. Agent Manager 不得要求 Manager 传长期明文密钥。
|
||||||
|
|
||||||
|
## 5. `GET /api/agnet/health`
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "agent-manager-swarm-runtime",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"runtime": "aks",
|
||||||
|
"time": "2026-05-28T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- HTTP 200。
|
||||||
|
- `status` 为 `healthy` / `ok` / `up` 之一。
|
||||||
|
- 不返回密钥、Token、连接串。
|
||||||
|
|
||||||
|
## 6. `POST /api/swarms`
|
||||||
|
|
||||||
|
### 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orchestration_plan": {
|
||||||
|
"intent_id": "task_123",
|
||||||
|
"template_hint": "heicode-task",
|
||||||
|
"objective": "完成本轮用户目标",
|
||||||
|
"sub_mode": "agile",
|
||||||
|
"risk_level": "medium",
|
||||||
|
"budget": {
|
||||||
|
"max_tokens": 120000,
|
||||||
|
"max_cost_usd": 8,
|
||||||
|
"max_duration_sec": 3600
|
||||||
|
},
|
||||||
|
"user_context": {
|
||||||
|
"user_id": "22",
|
||||||
|
"channel_id": "heicode",
|
||||||
|
"binding_scope": "task-task-123"
|
||||||
|
},
|
||||||
|
"billing_context": {
|
||||||
|
"provider": "newapi",
|
||||||
|
"default_model_id": "model_xxx",
|
||||||
|
"allowed_model_ids": ["model_xxx"],
|
||||||
|
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/model-gateway-key"
|
||||||
|
},
|
||||||
|
"agile_context": {
|
||||||
|
"iteration": "2026-05-28",
|
||||||
|
"stage": "development",
|
||||||
|
"checkpoint": "draft_created",
|
||||||
|
"acceptance_criteria": [
|
||||||
|
"接口返回成功",
|
||||||
|
"artifact 可回写到 timeline",
|
||||||
|
"不出现明文密钥"
|
||||||
|
],
|
||||||
|
"next_action": "continue",
|
||||||
|
"requires_user_approval": false
|
||||||
|
},
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"role_template": "backend",
|
||||||
|
"goal": "完成后端实现和测试",
|
||||||
|
"default_model_id": "model_xxx",
|
||||||
|
"resource_grants": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resource_grants": []
|
||||||
|
},
|
||||||
|
"agents": [
|
||||||
|
{
|
||||||
|
"role": "backend",
|
||||||
|
"resource_grants": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resource_grants": [
|
||||||
|
{
|
||||||
|
"grant_id": "grant-task-123-git",
|
||||||
|
"resource_id": "repo-main",
|
||||||
|
"resource_type": "git",
|
||||||
|
"permission_scope": ["repo:read", "repo:write:feature-branches"],
|
||||||
|
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-main",
|
||||||
|
"ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-main",
|
||||||
|
"target_role": "backend",
|
||||||
|
"constraints": {
|
||||||
|
"allowed_paths": "heicode/**"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"repo": "heicode-manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"callback": {
|
||||||
|
"url": "https://code.xinghanlab.com/api/agnet/callbacks/swarm-events",
|
||||||
|
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agnet-callback-signing-key",
|
||||||
|
"subscribed_events": [
|
||||||
|
"deployment.status_changed",
|
||||||
|
"task.created",
|
||||||
|
"task.claimed",
|
||||||
|
"task.heartbeat",
|
||||||
|
"task.blocked",
|
||||||
|
"task.retried",
|
||||||
|
"task.failed",
|
||||||
|
"task.completed",
|
||||||
|
"handoff.requested",
|
||||||
|
"handoff.completed",
|
||||||
|
"approval.requested",
|
||||||
|
"artifact.created",
|
||||||
|
"timeline.updated",
|
||||||
|
"budget.alert"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"manager_deployment_id": "dep_xxx",
|
||||||
|
"heicode_deployment_id": "dep_xxx",
|
||||||
|
"heicode_runtime_bridge": true,
|
||||||
|
"correlation_id": "corr_xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent Manager 必须消费的字段
|
||||||
|
|
||||||
|
| 字段 | 必须 | 说明 |
|
||||||
|
|---|---:|---|
|
||||||
|
| `orchestration_plan.objective` | 是 | 用户目标 |
|
||||||
|
| `orchestration_plan.sub_mode` | 是 | `agile` / `waterfall`;蜂群执行时也要保留此组织方式 |
|
||||||
|
| `orchestration_plan.risk_level` | 是 | 高危动作必须走审批 |
|
||||||
|
| `orchestration_plan.budget` | 是 | token / cost / duration 上限 |
|
||||||
|
| `billing_context.provider` | 是 | 当前为 `newapi` |
|
||||||
|
| `billing_context.secret_ref` | 是 | 模型网关密钥引用,只能是 `azkv://...` |
|
||||||
|
| `agents[].role` 或 `role_template` | 是 | 子 Agent 角色 |
|
||||||
|
| `resource_grants[]` | 是 | 资源授权清单 |
|
||||||
|
| `resource_grants[].secret_ref` | 凭据资源必填 | 只允许 Key Vault 引用,不允许明文 |
|
||||||
|
| `callback.url` | 是 | Runtime 回调 Manager 的地址 |
|
||||||
|
| `callback.signing_secret_ref` | 建议 | HMAC 签名密钥引用 |
|
||||||
|
| `metadata.manager_deployment_id` | 是 | Manager 侧 deployment id |
|
||||||
|
| `metadata.correlation_id` | 是 | 全链路追踪 |
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"deployment_id": "runtime-dep-123",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"status": "running",
|
||||||
|
"created_at": "2026-05-28T10:00:00Z",
|
||||||
|
"estimated_ready_at": "2026-05-28T10:02:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
兼容响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deployment_id": "runtime-dep-123",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"status": "running"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Manager 映射规则:
|
||||||
|
|
||||||
|
| Agent Manager 返回字段 | Manager 保存字段 |
|
||||||
|
|---|---|
|
||||||
|
| `data.deployment_id` / `deployment_id` / `id` | `runtime_deployment_id` |
|
||||||
|
| `data.swarm_id` / `swarm_id` / `runtime_swarm_id` | `runtime_swarm_id` |
|
||||||
|
| `data.status` / `status` / `runtime_status` | `runtime_state` |
|
||||||
|
|
||||||
|
## 7. Runtime 必须生成的 task graph
|
||||||
|
|
||||||
|
Agent Manager 创建 Swarm Run 后,必须在 Runtime 内部生成任务图,并通过 callback 回写。
|
||||||
|
|
||||||
|
每个 task 至少包含:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task_id": "task-backend-1",
|
||||||
|
"title": "实现后端接口",
|
||||||
|
"description": "完成 API、校验和测试",
|
||||||
|
"agent_role": "backend",
|
||||||
|
"status": "pending",
|
||||||
|
"depends_on": ["task-design-1"],
|
||||||
|
"attempt": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
状态建议:
|
||||||
|
|
||||||
|
| status | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `pending` | 等待执行 |
|
||||||
|
| `claimed` | 已被 Agent 领取 |
|
||||||
|
| `running` | 执行中 |
|
||||||
|
| `blocked` | 阻塞 |
|
||||||
|
| `handoff_requested` | 等待交接 |
|
||||||
|
| `retrying` | 重试中 |
|
||||||
|
| `completed` | 完成 |
|
||||||
|
| `failed` | 失败 |
|
||||||
|
|
||||||
|
## 8. Runtime 回调 Manager
|
||||||
|
|
||||||
|
Manager 回调地址:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
|
||||||
|
X-Agnet-Service-Token: <callback_token>
|
||||||
|
X-Agnet-Event-Id: <event_id>
|
||||||
|
X-Correlation-ID: <correlation_id>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
也支持 HMAC:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Agnet-Timestamp: <unix_ms>
|
||||||
|
X-Agnet-Signature: sha256=<hex>
|
||||||
|
```
|
||||||
|
|
||||||
|
HMAC 签名内容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
timestamp + "." + event_id + "." + raw_body
|
||||||
|
```
|
||||||
|
|
||||||
|
联调前可拉取 Manager 当前接受的事件 schema:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通用 callback envelope
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt_123",
|
||||||
|
"idempotency_key": "evt_123",
|
||||||
|
"event_type": "task.claimed",
|
||||||
|
"deployment_id": "dep_xxx",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"agent_instance_id": "agent-backend-1",
|
||||||
|
"task_id": "task-backend-1",
|
||||||
|
"occurred_at": "2026-05-28T10:10:00Z",
|
||||||
|
"correlation_id": "corr_xxx",
|
||||||
|
"source": "agent-manager-runtime",
|
||||||
|
"payload": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. `event_id` 全局唯一。
|
||||||
|
2. 重试同一事件必须复用相同 `event_id` 或 `idempotency_key`。
|
||||||
|
3. `deployment_id` 优先使用 Manager deployment id。
|
||||||
|
4. 如果只知道 `swarm_id`,Manager 也会按 `runtime_swarm_id` 查找 deployment。
|
||||||
|
5. `source` 要能区分真实 Runtime,例如 `agent-manager-runtime`,不要写 `simulated`。
|
||||||
|
|
||||||
|
## 9. 必须回调的事件和字段
|
||||||
|
|
||||||
|
| event_type | 必填字段 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `deployment.status_changed` | `payload.status` | Runtime 整体状态变化 |
|
||||||
|
| `task.created` | `task_id`, `payload.title` | 任务图新增任务 |
|
||||||
|
| `task.claimed` | `task_id`, `payload.agent_role` | Agent 领取任务 |
|
||||||
|
| `task.running` | `task_id`, `payload.agent_role` | Agent 开始执行 |
|
||||||
|
| `task.heartbeat` | `task_id`, `payload.agent_role` | Agent 心跳 |
|
||||||
|
| `task.blocked` | `task_id`, `payload.reason` | 任务阻塞 |
|
||||||
|
| `task.retried` | `task_id`, `payload.attempt` | 任务重试 |
|
||||||
|
| `task.failed` | `task_id`, `payload.reason` | 任务失败 |
|
||||||
|
| `task.completed` | `task_id` | 任务完成 |
|
||||||
|
| `handoff.requested` | `task_id`, `payload.from_role`, `payload.to_role` | 请求交接 |
|
||||||
|
| `handoff.completed` | `task_id`, `payload.from_role`, `payload.to_role` | 完成交接 |
|
||||||
|
| `artifact.created` | `artifact.artifact_id` 或 `payload.artifact_id` | 产物生成 |
|
||||||
|
| `approval.requested` | `payload.approval_id`, `payload.operation`, `payload.risk_level` | 高危审批 |
|
||||||
|
| `timeline.updated` | `payload.title` | 用户可见时间线 |
|
||||||
|
| `budget.alert` | `payload.threshold_pct` | 预算告警 |
|
||||||
|
|
||||||
|
## 10. artifact.created
|
||||||
|
|
||||||
|
Runtime 产生中间产物或最终交付物时回调:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt_artifact_1",
|
||||||
|
"event_type": "artifact.created",
|
||||||
|
"deployment_id": "dep_xxx",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"task_id": "task-backend-1",
|
||||||
|
"source": "agent-manager-runtime",
|
||||||
|
"artifact": {
|
||||||
|
"artifact_id": "art_backend_patch_1",
|
||||||
|
"artifact_type": "code_patch",
|
||||||
|
"title": "Backend API patch",
|
||||||
|
"summary": "新增任务创建和状态查询接口",
|
||||||
|
"uri": "git://heicode-manager/branches/feature/task-api",
|
||||||
|
"checksum": "sha256:abc123",
|
||||||
|
"metadata": {
|
||||||
|
"agent_role": "backend",
|
||||||
|
"redacted": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
支持的 `artifact_type`:
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `code_patch` | 代码补丁或分支 |
|
||||||
|
| `document` | 文档 |
|
||||||
|
| `test_report` | 测试报告 |
|
||||||
|
| `deployment_manifest` | 部署清单 |
|
||||||
|
| `log_bundle` | 日志包 |
|
||||||
|
| `other` | 其他 |
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. 大文件不要内联到 callback body。
|
||||||
|
2. `summary` 可展示给用户,不得包含密钥。
|
||||||
|
3. `uri` 可以是 `git://`、`artifact://`、`azblob://`、`https://`。
|
||||||
|
4. metadata 必须脱敏。
|
||||||
|
|
||||||
|
## 11. approval.requested 与审批结果
|
||||||
|
|
||||||
|
高危动作前 Runtime 必须暂停,并回调:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt_approval_1",
|
||||||
|
"event_type": "approval.requested",
|
||||||
|
"deployment_id": "dep_xxx",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"task_id": "task-deploy-1",
|
||||||
|
"source": "agent-manager-runtime",
|
||||||
|
"payload": {
|
||||||
|
"approval_id": "runtime-approval-1",
|
||||||
|
"operation": "deploy.production",
|
||||||
|
"resource_id": "prod-env",
|
||||||
|
"resource_type": "azure",
|
||||||
|
"resource_scope": "resource-group/heicode-prod",
|
||||||
|
"target_role": "ops",
|
||||||
|
"risk_level": "high",
|
||||||
|
"requires_credential": true,
|
||||||
|
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/prod-deploy",
|
||||||
|
"ttl_seconds": 600,
|
||||||
|
"reason": "需要部署到生产环境"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Manager / 客户端审批后,Manager 调 Runtime:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/swarms/{swarm_id}/approvals/{approval_id}
|
||||||
|
Authorization: Bearer <service_token>
|
||||||
|
X-Correlation-ID: <correlation_id>
|
||||||
|
X-Idempotency-Key: approval-decision-<approval_id>-<decision>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"approval_id": "runtime-approval-1",
|
||||||
|
"decision": "approved",
|
||||||
|
"reason": "用户已确认",
|
||||||
|
"manager_deployment_id": "dep_xxx",
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"decided_by": "user:22",
|
||||||
|
"decided_at": "2026-05-28T10:20:00Z",
|
||||||
|
"credential_lease": {
|
||||||
|
"lease_id": "lease_xxx",
|
||||||
|
"credential_ref": "lease://runtime/runtime-approval-1",
|
||||||
|
"expires_at": "2026-05-28T10:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`decision` 枚举:
|
||||||
|
|
||||||
|
| decision | Runtime 行为 |
|
||||||
|
|---|---|
|
||||||
|
| `approved` | 继续原高危动作 |
|
||||||
|
| `rejected` | 停止该动作,回调 `timeline.updated` 或 `task.failed` |
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
1. Runtime 收到 `approved` 后才可以继续高危动作。
|
||||||
|
2. Runtime 收到 `rejected` 后不能继续执行该动作。
|
||||||
|
3. Runtime 不得把 `credential_ref` 展开写入日志或 artifact metadata。
|
||||||
|
4. Runtime 必须把审批结果后的状态继续通过 callback 回写。
|
||||||
|
|
||||||
|
## 12. 日志、指标和用量
|
||||||
|
|
||||||
|
### 日志
|
||||||
|
|
||||||
|
建议提供:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/swarms/{swarm_id}/logs?limit=100&cursor=<cursor>
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T10:21:00Z",
|
||||||
|
"level": "info",
|
||||||
|
"agent_role": "backend",
|
||||||
|
"task_id": "task-backend-1",
|
||||||
|
"message": "test completed",
|
||||||
|
"redacted": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"next_cursor": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 指标
|
||||||
|
|
||||||
|
建议提供:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/swarms/{swarm_id}/metrics?window=15m&step=60s
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"swarm_id": "swarm-123",
|
||||||
|
"agent_metrics": [
|
||||||
|
{
|
||||||
|
"agent_instance_id": "agent-backend-1",
|
||||||
|
"agent_role": "backend",
|
||||||
|
"task_id": "task-backend-1",
|
||||||
|
"status": "running",
|
||||||
|
"cpu_percent": 12.5,
|
||||||
|
"memory_bytes": 268435456,
|
||||||
|
"uptime_seconds": 300
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 用量
|
||||||
|
|
||||||
|
Runtime 调模型时必须带关联字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `manager_deployment_id` | Manager deployment id |
|
||||||
|
| `swarm_id` | Runtime swarm id |
|
||||||
|
| `task_id` | 当前任务 |
|
||||||
|
| `agent_role` | Agent 角色 |
|
||||||
|
| `model_id` | 使用模型 |
|
||||||
|
| `correlation_id` | 全链路追踪 |
|
||||||
|
|
||||||
|
Runtime 可通过 `budget.alert` 或 `timeline.updated` 回传用量摘要。
|
||||||
|
|
||||||
|
## 13. 安全约束
|
||||||
|
|
||||||
|
必须遵守:
|
||||||
|
|
||||||
|
1. 禁止在请求、callback、日志、artifact metadata 中出现明文密码、Token、私钥、连接串、云 access key、模型 key。
|
||||||
|
2. 长期凭据只能通过 `azkv://<vault>/secrets/<name>` 传引用。
|
||||||
|
3. Runtime 可以把短期租约写成 `lease://...`,但不能写真实密钥。
|
||||||
|
4. `secret_ref`、`credential_ref` 只能作为引用使用,不能在用户可见内容中展开。
|
||||||
|
5. 所有 callback payload 必须脱敏。
|
||||||
|
6. 高危动作必须先 `approval.requested`,不能先执行后补审批。
|
||||||
|
|
||||||
|
## 14. 错误码建议
|
||||||
|
|
||||||
|
| HTTP | code | 场景 |
|
||||||
|
|---:|---|---|
|
||||||
|
| 401 | `UNAUTHORIZED` | service token 无效 |
|
||||||
|
| 403 | `FORBIDDEN` | 权限不足 |
|
||||||
|
| 404 | `SWARM_NOT_FOUND` | swarm 不存在 |
|
||||||
|
| 409 | `IDEMPOTENCY_CONFLICT` | 幂等键冲突 |
|
||||||
|
| 422 | `VALIDATION_ERROR` | 请求字段缺失 |
|
||||||
|
| 422 | `SECRET_REF_INVALID` | `secret_ref` 非 `azkv://...` |
|
||||||
|
| 422 | `POLICY_REJECTED` | 高危策略拒绝 |
|
||||||
|
| 500 | `INTERNAL_ERROR` | Runtime 内部错误 |
|
||||||
|
|
||||||
|
错误响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "resource_grants[0].secret_ref is required",
|
||||||
|
"request_id": "corr_xxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. 联调验收清单
|
||||||
|
|
||||||
|
| 步骤 | 操作 | 通过标准 |
|
||||||
|
|---:|---|---|
|
||||||
|
| 1 | Manager 调 `GET /api/agnet/health` | 返回 healthy |
|
||||||
|
| 2 | Manager 调 `POST /api/swarms` | 返回真实 `deployment_id` 和 `swarm_id` |
|
||||||
|
| 3 | Runtime 回调 `deployment.status_changed` | Manager timeline 可见 |
|
||||||
|
| 4 | Runtime 回调 `task.created` | Manager task flow / Agent task map 可见 |
|
||||||
|
| 5 | Runtime 回调 `task.claimed` / `task.heartbeat` | Manager 可看到 Agent 领取和心跳 |
|
||||||
|
| 6 | Runtime 回调 `handoff.requested` / `handoff.completed` | Manager 可看到交接 |
|
||||||
|
| 7 | Runtime 回调 `artifact.created` | Manager artifact 列表可见 |
|
||||||
|
| 8 | Runtime 回调 `approval.requested` | Manager approval 列表出现 pending |
|
||||||
|
| 9 | 客户端 / Manager approve | Runtime 收到 decision 并继续 |
|
||||||
|
| 10 | 客户端 / Manager reject | Runtime 收到 decision 并停止对应动作 |
|
||||||
|
| 11 | Runtime 回传 logs / metrics / usage | Manager 可查询或 timeline 可见 |
|
||||||
|
| 12 | Runtime 回调最终 `deployment.status_changed=completed` | Manager 和客户端看到完成 |
|
||||||
|
|
||||||
|
## 16. 当前不能误报完成的项
|
||||||
|
|
||||||
|
以下项只有在 Agent Manager 真实实现并联调后才能算完成:
|
||||||
|
|
||||||
|
1. 真实 Swarm Run 创建成功。
|
||||||
|
2. 真实 task graph 生成。
|
||||||
|
3. 真实 Agent claim / heartbeat。
|
||||||
|
4. 真实 handoff / retry / blocked。
|
||||||
|
5. 真实 artifact 产出。
|
||||||
|
6. 审批后 Runtime 继续或停止。
|
||||||
|
7. Runtime 真实日志和指标。
|
||||||
|
8. 子 Agent 模型用量归属。
|
||||||
|
9. 最终交付物回到客户端。
|
||||||
|
|
||||||
|
Manager 本地模拟事件、测试桩、空态页面和 schema 校验只能证明 Manager 接收和展示能力,不能证明蜂群生产闭环完成。
|
||||||
|
|
||||||
@@ -252,6 +252,14 @@ Agent Manager 至少要支持以下阶段语义,并通过 callback 回写当
|
|||||||
|
|
||||||
普通 sub 敏捷下,客户端主要看 timeline。Agent Manager 应持续回写可展示事件。
|
普通 sub 敏捷下,客户端主要看 timeline。Agent Manager 应持续回写可展示事件。
|
||||||
|
|
||||||
|
联调前可以先请求 Manager 当前接受的 callback schema:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
|
||||||
|
```
|
||||||
|
|
||||||
|
该接口只返回事件类型、分类和必填字段,不返回任何 token 或密钥。普通 sub 敏捷重点核对 `phase.changed`、`timeline.updated`、`artifact.created`、`approval.requested`、`sk_tool.*` 和 `budget.alert`。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event_id": "evt_timeline_1",
|
"event_id": "evt_timeline_1",
|
||||||
|
|||||||
@@ -200,6 +200,14 @@ X-Agnet-Signature: sha256=<hex>
|
|||||||
timestamp + "." + event_id + "." + raw_body
|
timestamp + "." + event_id + "." + raw_body
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Agent Manager / 蜂群侧可以先拉取 Manager 当前接受的事件 schema:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
|
||||||
|
```
|
||||||
|
|
||||||
|
该接口不返回任何 token 或密钥,只返回事件类型、分类和必填字段,用于联调前核对 `task.*`、`handoff.*`、`artifact.created`、`approval.requested` 等 callback payload 是否符合 Manager 接收规则。
|
||||||
|
|
||||||
统一 envelope:
|
统一 envelope:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -329,18 +330,90 @@ func callbackIntValue(values map[string]any, key string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var agnetCallbackEventRequiredFields = map[string][]string{
|
var agnetCallbackEventRequiredFields = map[string][]string{
|
||||||
"task.created": {"task_id", "title"},
|
"deployment.status_changed": {"status"},
|
||||||
"task.claimed": {"task_id", "agent_role"},
|
"phase.changed": {"stage", "checkpoint"},
|
||||||
"task.running": {"task_id", "agent_role"},
|
"agent.started": {"agent_role"},
|
||||||
"task.heartbeat": {"task_id", "agent_role"},
|
"agent.completed": {"agent_role"},
|
||||||
"task.blocked": {"task_id", "reason"},
|
"agent.crashed": {"agent_role", "reason"},
|
||||||
"task.retried": {"task_id", "attempt"},
|
"task.created": {"task_id", "title"},
|
||||||
"task.released": {"task_id", "agent_role"},
|
"task.claimed": {"task_id", "agent_role"},
|
||||||
"task.failed": {"task_id", "reason"},
|
"task.running": {"task_id", "agent_role"},
|
||||||
"task.completed": {"task_id"},
|
"task.heartbeat": {"task_id", "agent_role"},
|
||||||
"handoff.requested": {"task_id", "from_role", "to_role"},
|
"task.blocked": {"task_id", "reason"},
|
||||||
"handoff.completed": {"task_id", "from_role", "to_role"},
|
"task.retried": {"task_id", "attempt"},
|
||||||
"artifact.created": {"artifact_id"},
|
"task.released": {"task_id", "agent_role"},
|
||||||
|
"task.failed": {"task_id", "reason"},
|
||||||
|
"task.completed": {"task_id"},
|
||||||
|
"handoff.requested": {"task_id", "from_role", "to_role"},
|
||||||
|
"handoff.completed": {"task_id", "from_role", "to_role"},
|
||||||
|
"approval.requested": {"approval_id", "operation", "risk_level"},
|
||||||
|
"artifact.created": {"artifact_id"},
|
||||||
|
"timeline.updated": {"title"},
|
||||||
|
"sk_tool.called": {"tool_name", "tool_invocation_id"},
|
||||||
|
"sk_tool.completed": {"tool_name", "tool_invocation_id"},
|
||||||
|
"sk_tool.failed": {"tool_name", "tool_invocation_id", "reason"},
|
||||||
|
"budget.alert": {"threshold_pct"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var agnetCallbackEventCategories = map[string]string{
|
||||||
|
"deployment.status_changed": "deployment",
|
||||||
|
"phase.changed": "ordinary_sub",
|
||||||
|
"agent.started": "ordinary_sub",
|
||||||
|
"agent.completed": "ordinary_sub",
|
||||||
|
"agent.crashed": "ordinary_sub",
|
||||||
|
"task.created": "swarm_task_flow",
|
||||||
|
"task.claimed": "swarm_task_flow",
|
||||||
|
"task.running": "swarm_task_flow",
|
||||||
|
"task.heartbeat": "swarm_task_flow",
|
||||||
|
"task.blocked": "swarm_task_flow",
|
||||||
|
"task.retried": "swarm_task_flow",
|
||||||
|
"task.released": "swarm_task_flow",
|
||||||
|
"task.failed": "swarm_task_flow",
|
||||||
|
"task.completed": "swarm_task_flow",
|
||||||
|
"handoff.requested": "swarm_task_flow",
|
||||||
|
"handoff.completed": "swarm_task_flow",
|
||||||
|
"approval.requested": "approval",
|
||||||
|
"artifact.created": "artifact",
|
||||||
|
"timeline.updated": "timeline",
|
||||||
|
"sk_tool.called": "sk",
|
||||||
|
"sk_tool.completed": "sk",
|
||||||
|
"sk_tool.failed": "sk",
|
||||||
|
"budget.alert": "budget",
|
||||||
|
}
|
||||||
|
|
||||||
|
func AgnetGetSwarmEventCallbackSchema(c *gin.Context) {
|
||||||
|
events := make([]string, 0, len(agnetCallbackEventRequiredFields))
|
||||||
|
for eventType := range agnetCallbackEventRequiredFields {
|
||||||
|
events = append(events, eventType)
|
||||||
|
}
|
||||||
|
sort.Strings(events)
|
||||||
|
|
||||||
|
items := make([]gin.H, 0, len(events))
|
||||||
|
for _, eventType := range events {
|
||||||
|
required := append([]string(nil), agnetCallbackEventRequiredFields[eventType]...)
|
||||||
|
sort.Strings(required)
|
||||||
|
items = append(items, gin.H{
|
||||||
|
"event_type": eventType,
|
||||||
|
"category": agnetCallbackEventCategories[eventType],
|
||||||
|
"required_fields": required,
|
||||||
|
"payload_location": "top-level envelope or payload object; artifact_id may also be in artifact object",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
common.ApiSuccess(c, gin.H{
|
||||||
|
"callback_path": "/api/agnet/callbacks/swarm-events",
|
||||||
|
"auth": gin.H{
|
||||||
|
"service_token_headers": []string{"X-Agnet-Service-Token", "Authorization: Bearer <token>"},
|
||||||
|
"hmac_headers": []string{"X-Agnet-Event-Id", "X-Agnet-Timestamp", "X-Agnet-Signature"},
|
||||||
|
"hmac_payload": "timestamp + \".\" + event_id + \".\" + raw_body",
|
||||||
|
},
|
||||||
|
"dedupe_keys": []string{"X-Agnet-Event-Id", "event_id", "idempotency_key"},
|
||||||
|
"events": items,
|
||||||
|
"security": gin.H{
|
||||||
|
"plaintext_secrets_allowed": false,
|
||||||
|
"secret_ref_scheme": "azkv://<vault>/secrets/<name>",
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) string {
|
func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) string {
|
||||||
@@ -361,9 +434,11 @@ func callbackHasFieldValue(payload agnetCallbackEnvelope, key string) bool {
|
|||||||
if callbackStringValue(payload.Payload, key) != "" {
|
if callbackStringValue(payload.Payload, key) != "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if key == "artifact_id" {
|
for _, nestedKey := range []string{"artifact", "approval"} {
|
||||||
if nested := callbackMapValue(payload.Payload, "artifact"); nested != nil {
|
if nested := callbackMapValue(payload.Payload, nestedKey); nested != nil {
|
||||||
return callbackStringValue(nested, key) != ""
|
if callbackStringValue(nested, key) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if key == "attempt" {
|
if key == "attempt" {
|
||||||
|
|||||||
@@ -1639,6 +1639,8 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
"timestamp": record.CreatedAt,
|
"timestamp": record.CreatedAt,
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
"stream": "control",
|
"stream": "control",
|
||||||
|
"source": "manager_control_plane",
|
||||||
|
"data_source": "manager_control_plane",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"message": "deployment accepted by Manager control-plane placeholder",
|
"message": "deployment accepted by Manager control-plane placeholder",
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
@@ -1652,6 +1654,8 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
"timestamp": event.OccurredAt,
|
"timestamp": event.OccurredAt,
|
||||||
"deployment_id": event.DeploymentID,
|
"deployment_id": event.DeploymentID,
|
||||||
"stream": "event",
|
"stream": "event",
|
||||||
|
"source": "manager_audit",
|
||||||
|
"data_source": "manager_audit",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"message": event.Event,
|
"message": event.Event,
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
@@ -1663,11 +1667,13 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
"items": items,
|
"items": items,
|
||||||
"next_cursor": "",
|
"next_cursor": "",
|
||||||
"redacted": true,
|
"redacted": true,
|
||||||
"total": len(items),
|
"data_source": "manager_control_plane",
|
||||||
|
"runtime_source": "not_connected",
|
||||||
|
"total": len(items),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,6 +1699,8 @@ func AgnetGetDeploymentMetrics(c *gin.Context) {
|
|||||||
|
|
||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
|
"data_source": "manager_control_plane",
|
||||||
|
"runtime_source": "not_connected",
|
||||||
"window": strings.TrimSpace(c.DefaultQuery("window", "15m")),
|
"window": strings.TrimSpace(c.DefaultQuery("window", "15m")),
|
||||||
"step": strings.TrimSpace(c.DefaultQuery("step", "60s")),
|
"step": strings.TrimSpace(c.DefaultQuery("step", "60s")),
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
|
|||||||
@@ -156,6 +156,18 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
|
|||||||
require.Contains(t, healthBody, `"success":true`)
|
require.Contains(t, healthBody, `"success":true`)
|
||||||
require.Contains(t, healthBody, `"status":"healthy"`)
|
require.Contains(t, healthBody, `"status":"healthy"`)
|
||||||
|
|
||||||
|
schemaResp, err := http.Get(managerURL + "/api/agnet/callbacks/swarm-events/schema")
|
||||||
|
require.NoError(t, err)
|
||||||
|
schemaBody := readAgnetRuntimeSmokeBody(t, schemaResp)
|
||||||
|
require.Equal(t, http.StatusOK, schemaResp.StatusCode)
|
||||||
|
require.Contains(t, schemaBody, `"event_type":"task.claimed"`)
|
||||||
|
require.Contains(t, schemaBody, `"event_type":"approval.requested"`)
|
||||||
|
var schemaEnvelope map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal([]byte(schemaBody), &schemaEnvelope))
|
||||||
|
schemaData := schemaEnvelope["data"].(map[string]any)
|
||||||
|
schemaSecurity := schemaData["security"].(map[string]any)
|
||||||
|
require.Equal(t, "azkv://<vault>/secrets/<name>", schemaSecurity["secret_ref_scheme"])
|
||||||
|
|
||||||
createBody := `{
|
createBody := `{
|
||||||
"orchestration_plan":{
|
"orchestration_plan":{
|
||||||
"intent_id":"intent-http-smoke",
|
"intent_id":"intent-http-smoke",
|
||||||
@@ -184,4 +196,16 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
|
|||||||
var stored model.AgnetDeployment
|
var stored model.AgnetDeployment
|
||||||
require.NoError(t, db.Where("runtime_swarm_id = ?", "runtime-http-swarm").First(&stored).Error)
|
require.NoError(t, db.Where("runtime_swarm_id = ?", "runtime-http-swarm").First(&stored).Error)
|
||||||
require.Equal(t, "accepted", stored.RuntimeState)
|
require.Equal(t, "accepted", stored.RuntimeState)
|
||||||
|
|
||||||
|
logsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/logs", "")
|
||||||
|
logsBody := readAgnetRuntimeSmokeBody(t, logsResp)
|
||||||
|
require.Equal(t, http.StatusOK, logsResp.StatusCode)
|
||||||
|
require.Contains(t, logsBody, `"data_source":"manager_control_plane"`)
|
||||||
|
require.Contains(t, logsBody, `"runtime_source":"not_connected"`)
|
||||||
|
|
||||||
|
metricsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/metrics", "")
|
||||||
|
metricsBody := readAgnetRuntimeSmokeBody(t, metricsResp)
|
||||||
|
require.Equal(t, http.StatusOK, metricsResp.StatusCode)
|
||||||
|
require.Contains(t, metricsBody, `"data_source":"manager_control_plane"`)
|
||||||
|
require.Contains(t, metricsBody, `"platform_estimated":true`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
||||||
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
||||||
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
|
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
|
||||||
|
apiRouter.GET("/agnet/callbacks/swarm-events/schema", controller.AgnetGetSwarmEventCallbackSchema)
|
||||||
apiRouter.POST("/agnet/callbacks/swarm-events", controller.AgnetReceiveSwarmEventCallback)
|
apiRouter.POST("/agnet/callbacks/swarm-events", controller.AgnetReceiveSwarmEventCallback)
|
||||||
apiRouter.POST("/swarms", middleware.UserAuth(), controller.AgnetCreateUserSwarm)
|
apiRouter.POST("/swarms", middleware.UserAuth(), controller.AgnetCreateUserSwarm)
|
||||||
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
|
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
|
||||||
|
|||||||
+116
-8
@@ -333,6 +333,54 @@ function taskFlowDetail(item: Record<string, unknown>): string {
|
|||||||
return parts.join(' / ') || '—'
|
return parts.join(' / ') || '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskFlowSummary = {
|
||||||
|
taskId: string
|
||||||
|
status: string
|
||||||
|
agentRole: string
|
||||||
|
handoff: string
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTaskFlowSummaries(
|
||||||
|
records: Record<string, unknown>[]
|
||||||
|
): TaskFlowSummary[] {
|
||||||
|
const byTask = new Map<string, TaskFlowSummary>()
|
||||||
|
records.forEach((item, idx) => {
|
||||||
|
const event = String(item.event_type || item.event || '').toLowerCase()
|
||||||
|
const taskId =
|
||||||
|
String(recordPayloadValue(item, 'task_id') || '').trim() ||
|
||||||
|
`task-${idx + 1}`
|
||||||
|
const current =
|
||||||
|
byTask.get(taskId) ??
|
||||||
|
({
|
||||||
|
taskId,
|
||||||
|
status: 'observed',
|
||||||
|
agentRole: '',
|
||||||
|
handoff: '',
|
||||||
|
source: formatRecordSource(item.source),
|
||||||
|
} satisfies TaskFlowSummary)
|
||||||
|
const agentRole = String(recordPayloadValue(item, 'agent_role') || '')
|
||||||
|
const fromRole = String(recordPayloadValue(item, 'from_role') || '')
|
||||||
|
const toRole = String(recordPayloadValue(item, 'to_role') || '')
|
||||||
|
const reason = String(recordPayloadValue(item, 'reason') || '')
|
||||||
|
if (agentRole) current.agentRole = agentRole
|
||||||
|
if (event.startsWith('task.')) {
|
||||||
|
current.status = event.replace('task.', '') || current.status
|
||||||
|
}
|
||||||
|
if (event === 'handoff.requested' || event === 'handoff.completed') {
|
||||||
|
current.status = event.replace('handoff.', 'handoff:')
|
||||||
|
if (fromRole || toRole)
|
||||||
|
current.handoff = `${fromRole || '?'} -> ${toRole || '?'}`
|
||||||
|
}
|
||||||
|
if (reason && current.status === 'blocked') {
|
||||||
|
current.handoff = reason
|
||||||
|
}
|
||||||
|
current.source = formatRecordSource(item.source)
|
||||||
|
byTask.set(taskId, current)
|
||||||
|
})
|
||||||
|
return Array.from(byTask.values())
|
||||||
|
}
|
||||||
|
|
||||||
function collectResourceGrants(
|
function collectResourceGrants(
|
||||||
dep: AgnetDeployment
|
dep: AgnetDeployment
|
||||||
): Record<string, unknown>[] {
|
): Record<string, unknown>[] {
|
||||||
@@ -1001,11 +1049,13 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
const timeline = data?.timeline ?? []
|
const timeline = data?.timeline ?? []
|
||||||
const taskFlowRecords = useMemo(
|
const taskFlowRecords = useMemo(
|
||||||
() =>
|
() =>
|
||||||
timeline.filter((item) =>
|
timeline.filter((item) => isTaskFlowEvent(item.event_type || item.event)),
|
||||||
isTaskFlowEvent(item.event_type || item.event)
|
|
||||||
),
|
|
||||||
[timeline]
|
[timeline]
|
||||||
)
|
)
|
||||||
|
const taskFlowSummaries = useMemo(
|
||||||
|
() => buildTaskFlowSummaries(taskFlowRecords),
|
||||||
|
[taskFlowRecords]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_52%,transparent)] p-4'>
|
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_52%,transparent)] p-4'>
|
||||||
@@ -1046,12 +1096,12 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
<div className='bg-background/45 mt-4 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
||||||
<div className='flex items-center justify-between gap-2'>
|
<div className='flex items-center justify-between gap-2'>
|
||||||
<p className='text-foreground text-xs font-semibold'>
|
<p className='text-foreground text-xs font-semibold'>
|
||||||
{t('Sub task flow')}
|
{t('Sub task flow')}
|
||||||
</p>
|
</p>
|
||||||
<span className='text-muted-foreground rounded-full bg-background/60 px-2 py-0.5 text-[10px]'>
|
<span className='text-muted-foreground bg-background/60 rounded-full px-2 py-0.5 text-[10px]'>
|
||||||
{taskFlowRecords.length}
|
{taskFlowRecords.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1074,7 +1124,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
<p className='min-w-0 truncate font-mono text-[11px]'>
|
<p className='min-w-0 truncate font-mono text-[11px]'>
|
||||||
{event}
|
{event}
|
||||||
</p>
|
</p>
|
||||||
<span className='text-muted-foreground shrink-0 rounded-full bg-background/60 px-2 py-0.5 text-[10px]'>
|
<span className='text-muted-foreground bg-background/60 shrink-0 rounded-full px-2 py-0.5 text-[10px]'>
|
||||||
{source}
|
{source}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1088,6 +1138,64 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-background/45 mt-4 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
||||||
|
<div className='flex items-center justify-between gap-2'>
|
||||||
|
<p className='text-foreground text-xs font-semibold'>
|
||||||
|
{t('Agent task map')}
|
||||||
|
</p>
|
||||||
|
<span className='text-muted-foreground bg-background/60 rounded-full px-2 py-0.5 text-[10px]'>
|
||||||
|
{taskFlowSummaries.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{taskFlowSummaries.length === 0 ? (
|
||||||
|
<p className='text-muted-foreground mt-2 text-xs'>
|
||||||
|
{t(
|
||||||
|
'Waiting for Runtime task graph callbacks. Manager will show task, Agent role, handoff and source here when callbacks arrive.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className='mt-2 overflow-x-auto'>
|
||||||
|
<table className='w-full min-w-[520px] border-collapse text-left text-[11px]'>
|
||||||
|
<thead className='text-muted-foreground'>
|
||||||
|
<tr>
|
||||||
|
<th className='py-1 pr-3 font-medium'>{t('Task')}</th>
|
||||||
|
<th className='py-1 pr-3 font-medium'>{t('Status')}</th>
|
||||||
|
<th className='py-1 pr-3 font-medium'>{t('Agent role')}</th>
|
||||||
|
<th className='py-1 pr-3 font-medium'>{t('Handoff')}</th>
|
||||||
|
<th className='py-1 pr-3 font-medium'>{t('Source')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{taskFlowSummaries.map((item) => (
|
||||||
|
<tr
|
||||||
|
key={item.taskId}
|
||||||
|
className='border-t border-dashed border-[color-mix(in_oklch,var(--primary)_14%,var(--border))]'
|
||||||
|
>
|
||||||
|
<td className='py-2 pr-3 font-mono text-[10px]'>
|
||||||
|
{item.taskId}
|
||||||
|
</td>
|
||||||
|
<td className='py-2 pr-3'>
|
||||||
|
<StatusBadge phase={item.status} />
|
||||||
|
</td>
|
||||||
|
<td className='text-muted-foreground py-2 pr-3 font-mono text-[10px]'>
|
||||||
|
{item.agentRole || '—'}
|
||||||
|
</td>
|
||||||
|
<td className='text-muted-foreground py-2 pr-3 text-[10px]'>
|
||||||
|
{item.handoff || '—'}
|
||||||
|
</td>
|
||||||
|
<td className='py-2 pr-3'>
|
||||||
|
<span className='text-muted-foreground bg-background/60 rounded-full px-2 py-0.5 text-[10px]'>
|
||||||
|
{item.source}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-3 md:grid-cols-2'>
|
<div className='mt-4 grid gap-3 md:grid-cols-2'>
|
||||||
<div className='bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
<div className='bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
||||||
<p className='text-foreground text-xs font-semibold'>
|
<p className='text-foreground text-xs font-semibold'>
|
||||||
@@ -1159,7 +1267,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-3 bg-background/45 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
<div className='bg-background/45 mt-3 rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-3'>
|
||||||
<p className='text-foreground text-xs font-semibold'>
|
<p className='text-foreground text-xs font-semibold'>
|
||||||
{t('Merged timeline')}
|
{t('Merged timeline')}
|
||||||
</p>
|
</p>
|
||||||
@@ -1179,7 +1287,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
|
|||||||
>
|
>
|
||||||
<div className='flex items-center justify-between gap-2'>
|
<div className='flex items-center justify-between gap-2'>
|
||||||
<p className='truncate font-mono text-[11px]'>{event}</p>
|
<p className='truncate font-mono text-[11px]'>{event}</p>
|
||||||
<span className='text-muted-foreground shrink-0 rounded-full bg-background/60 px-2 py-0.5 text-[10px]'>
|
<span className='text-muted-foreground bg-background/60 shrink-0 rounded-full px-2 py-0.5 text-[10px]'>
|
||||||
{source}
|
{source}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ authenticated deployment flow, provide:
|
|||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
HEICODE_RUN_SIMULATE=true
|
HEICODE_RUN_SIMULATE=true
|
||||||
|
HEICODE_RUN_CALLBACK_SMOKE=true
|
||||||
|
HEICODE_CALLBACK_TOKEN=<Manager callback service token>
|
||||||
|
HEICODE_CALLBACK_SWARM_ID=<runtime swarm id, optional>
|
||||||
HEICODE_MANAGER_BASE_URL=https://code.xinghanlab.com
|
HEICODE_MANAGER_BASE_URL=https://code.xinghanlab.com
|
||||||
AGENT_MANAGER_HEALTH_URL=http://20.212.121.126/api/agnet/health
|
AGENT_MANAGER_HEALTH_URL=http://20.212.121.126/api/agnet/health
|
||||||
"""
|
"""
|
||||||
@@ -31,11 +34,18 @@ AGENT_HEALTH = os.getenv(
|
|||||||
ACCESS_TOKEN = os.getenv("HEICODE_ACCESS_TOKEN", "").strip()
|
ACCESS_TOKEN = os.getenv("HEICODE_ACCESS_TOKEN", "").strip()
|
||||||
USER_ID = os.getenv("HEICODE_USER_ID", "").strip()
|
USER_ID = os.getenv("HEICODE_USER_ID", "").strip()
|
||||||
DEPLOYMENT_ID = os.getenv("HEICODE_DEPLOYMENT_ID", "").strip()
|
DEPLOYMENT_ID = os.getenv("HEICODE_DEPLOYMENT_ID", "").strip()
|
||||||
|
CALLBACK_TOKEN = os.getenv("HEICODE_CALLBACK_TOKEN", "").strip()
|
||||||
|
CALLBACK_SWARM_ID = os.getenv("HEICODE_CALLBACK_SWARM_ID", "").strip()
|
||||||
RUN_SIMULATE = os.getenv("HEICODE_RUN_SIMULATE", "").strip().lower() in {
|
RUN_SIMULATE = os.getenv("HEICODE_RUN_SIMULATE", "").strip().lower() in {
|
||||||
"1",
|
"1",
|
||||||
"true",
|
"true",
|
||||||
"yes",
|
"yes",
|
||||||
}
|
}
|
||||||
|
RUN_CALLBACK_SMOKE = os.getenv("HEICODE_RUN_CALLBACK_SMOKE", "").strip().lower() in {
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
}
|
||||||
UA = "heicode-agnet-sub-mode-smoke/1.0"
|
UA = "heicode-agnet-sub-mode-smoke/1.0"
|
||||||
FAILS: list[str] = []
|
FAILS: list[str] = []
|
||||||
|
|
||||||
@@ -93,6 +103,23 @@ check("Manager /api/status success", code == 200 and status.get("success") is Tr
|
|||||||
version = status.get("data", {}).get("version") if isinstance(status.get("data"), dict) else ""
|
version = status.get("data", {}).get("version") if isinstance(status.get("data"), dict) else ""
|
||||||
check("Manager status carries version", bool(version), f"version={version!r}")
|
check("Manager status carries version", bool(version), f"version={version!r}")
|
||||||
|
|
||||||
|
code, _headers, body = manager_request("GET", "/api/agnet/callbacks/swarm-events/schema")
|
||||||
|
schema = parse_json("callback schema JSON parseable", body)
|
||||||
|
schema_data = schema.get("data", {}) if isinstance(schema.get("data"), dict) else {}
|
||||||
|
schema_events = schema_data.get("events") or []
|
||||||
|
schema_event_types = {
|
||||||
|
item.get("event_type")
|
||||||
|
for item in schema_events
|
||||||
|
if isinstance(item, dict)
|
||||||
|
}
|
||||||
|
check(
|
||||||
|
"callback schema lists swarm and ordinary sub events",
|
||||||
|
code == 200
|
||||||
|
and schema.get("success") is True
|
||||||
|
and {"task.claimed", "handoff.requested", "phase.changed", "artifact.created"}.issubset(schema_event_types),
|
||||||
|
f"HTTP {code}",
|
||||||
|
)
|
||||||
|
|
||||||
code, _headers, body = request("GET", AGENT_HEALTH)
|
code, _headers, body = request("GET", AGENT_HEALTH)
|
||||||
agent = parse_json("Agent Manager health JSON parseable", body)
|
agent = parse_json("Agent Manager health JSON parseable", body)
|
||||||
agent_data = agent.get("data", {}) if isinstance(agent.get("data"), dict) else {}
|
agent_data = agent.get("data", {}) if isinstance(agent.get("data"), dict) else {}
|
||||||
@@ -106,6 +133,91 @@ check(
|
|||||||
if not (ACCESS_TOKEN and USER_ID and DEPLOYMENT_ID):
|
if not (ACCESS_TOKEN and USER_ID and DEPLOYMENT_ID):
|
||||||
print("\nAuthenticated deployment checks skipped: set HEICODE_ACCESS_TOKEN, HEICODE_USER_ID and HEICODE_DEPLOYMENT_ID.")
|
print("\nAuthenticated deployment checks skipped: set HEICODE_ACCESS_TOKEN, HEICODE_USER_ID and HEICODE_DEPLOYMENT_ID.")
|
||||||
else:
|
else:
|
||||||
|
if RUN_CALLBACK_SMOKE:
|
||||||
|
if not CALLBACK_TOKEN:
|
||||||
|
check("callback smoke has HEICODE_CALLBACK_TOKEN", False)
|
||||||
|
else:
|
||||||
|
callback_headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Agnet-Service-Token": CALLBACK_TOKEN,
|
||||||
|
"X-Correlation-ID": f"smoke-corr-{DEPLOYMENT_ID}",
|
||||||
|
}
|
||||||
|
swarm_id = CALLBACK_SWARM_ID or f"smoke-swarm-{DEPLOYMENT_ID}"
|
||||||
|
callback_events = [
|
||||||
|
{
|
||||||
|
"event_id": f"smoke-task-created-{DEPLOYMENT_ID}",
|
||||||
|
"event_type": "task.created",
|
||||||
|
"deployment_id": DEPLOYMENT_ID,
|
||||||
|
"swarm_id": swarm_id,
|
||||||
|
"task_id": f"smoke-task-{DEPLOYMENT_ID}",
|
||||||
|
"source": "smoke-runtime",
|
||||||
|
"payload": {
|
||||||
|
"title": "Smoke task created",
|
||||||
|
"agent_role": "backend",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event_id": f"smoke-task-claimed-{DEPLOYMENT_ID}",
|
||||||
|
"event_type": "task.claimed",
|
||||||
|
"deployment_id": DEPLOYMENT_ID,
|
||||||
|
"swarm_id": swarm_id,
|
||||||
|
"task_id": f"smoke-task-{DEPLOYMENT_ID}",
|
||||||
|
"source": "smoke-runtime",
|
||||||
|
"payload": {
|
||||||
|
"agent_role": "backend",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event_id": f"smoke-handoff-{DEPLOYMENT_ID}",
|
||||||
|
"event_type": "handoff.requested",
|
||||||
|
"deployment_id": DEPLOYMENT_ID,
|
||||||
|
"swarm_id": swarm_id,
|
||||||
|
"task_id": f"smoke-task-{DEPLOYMENT_ID}",
|
||||||
|
"source": "smoke-runtime",
|
||||||
|
"payload": {
|
||||||
|
"from_role": "backend",
|
||||||
|
"to_role": "reviewer",
|
||||||
|
"reason": "Smoke handoff validation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event_id": f"smoke-artifact-{DEPLOYMENT_ID}",
|
||||||
|
"event_type": "artifact.created",
|
||||||
|
"deployment_id": DEPLOYMENT_ID,
|
||||||
|
"swarm_id": swarm_id,
|
||||||
|
"task_id": f"smoke-task-{DEPLOYMENT_ID}",
|
||||||
|
"source": "smoke-runtime",
|
||||||
|
"artifact": {
|
||||||
|
"artifact_id": f"smoke-artifact-{DEPLOYMENT_ID}",
|
||||||
|
"artifact_type": "test_report",
|
||||||
|
"title": "Smoke artifact",
|
||||||
|
"summary": "Callback smoke wrote this artifact through the Manager callback endpoint.",
|
||||||
|
"uri": f"artifact://smoke/{DEPLOYMENT_ID}/report",
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"stage": "testing",
|
||||||
|
"checkpoint": "artifact_ready",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for event in callback_events:
|
||||||
|
body_bytes = json.dumps(event).encode("utf-8")
|
||||||
|
callback_headers["X-Agnet-Event-Id"] = event["event_id"]
|
||||||
|
code, _headers, body = request(
|
||||||
|
"POST",
|
||||||
|
BASE + "/api/agnet/callbacks/swarm-events",
|
||||||
|
headers=callback_headers,
|
||||||
|
body=body_bytes,
|
||||||
|
)
|
||||||
|
callback_response = parse_json(
|
||||||
|
f"callback {event['event_type']} JSON parseable", body
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
f"callback {event['event_type']} accepted",
|
||||||
|
code == 200 and callback_response.get("success") is True,
|
||||||
|
f"HTTP {code}",
|
||||||
|
)
|
||||||
|
|
||||||
if RUN_SIMULATE:
|
if RUN_SIMULATE:
|
||||||
code, _headers, body = manager_request(
|
code, _headers, body = manager_request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -155,6 +267,11 @@ else:
|
|||||||
for item in merged
|
for item in merged
|
||||||
if isinstance(item, dict)
|
if isinstance(item, dict)
|
||||||
}
|
}
|
||||||
|
sources = {
|
||||||
|
item.get("source")
|
||||||
|
for item in merged
|
||||||
|
if isinstance(item, dict) and item.get("source")
|
||||||
|
}
|
||||||
check(
|
check(
|
||||||
"timeline endpoint success",
|
"timeline endpoint success",
|
||||||
code == 200 and timeline.get("success") is True and isinstance(merged, list),
|
code == 200 and timeline.get("success") is True and isinstance(merged, list),
|
||||||
@@ -170,6 +287,21 @@ else:
|
|||||||
"artifact.created" in event_types or "approval.requested" in event_types,
|
"artifact.created" in event_types or "approval.requested" in event_types,
|
||||||
"events=" + ",".join(sorted(str(event) for event in event_types if event)[:10]),
|
"events=" + ",".join(sorted(str(event) for event in event_types if event)[:10]),
|
||||||
)
|
)
|
||||||
|
if RUN_CALLBACK_SMOKE:
|
||||||
|
check(
|
||||||
|
"timeline includes smoke callback source",
|
||||||
|
"smoke-runtime" in sources,
|
||||||
|
"sources=" + ",".join(sorted(str(source) for source in sources)[:10]),
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
"callback smoke artifact is queryable",
|
||||||
|
any(
|
||||||
|
isinstance(item, dict)
|
||||||
|
and item.get("artifact_id") == f"smoke-artifact-{DEPLOYMENT_ID}"
|
||||||
|
for item in artifact_items
|
||||||
|
),
|
||||||
|
f"artifact_count={len(artifact_items) if isinstance(artifact_items, list) else 'n/a'}",
|
||||||
|
)
|
||||||
|
|
||||||
if FAILS:
|
if FAILS:
|
||||||
print(f"\n{len(FAILS)} check(s) failed:")
|
print(f"\n{len(FAILS)} check(s) failed:")
|
||||||
|
|||||||
Reference in New Issue
Block a user