fix: sync sub runtime callback state
This commit is contained in:
@@ -8,34 +8,31 @@
|
||||
|
||||
Agent Manager 新版文档 `HEICODE_API_INTEGRATION(5).md` 已经补充了 Runtime 主动 callback、HMAC、artifact、timeline、SK snapshot 等内容,方向基本对齐 Heicode Manager。
|
||||
|
||||
但按 2026-05-28 实测结果,目前还不能认为普通 sub 敏捷模式已经完整闭环。主要问题是:
|
||||
按 2026-05-28 生产实测,Manager 与 Agent Manager 的普通 sub 核心通讯链路已经跑通,但还不能认为“真实开发执行结果”完整闭环。当前结论是:
|
||||
|
||||
1. Agent Manager 当前联调统一使用 IP `http://20.212.121.126`;域名 `https://agent-manager.taijiagnet.com` 后续解析和证书就绪后再切换,不作为当前联调阻塞项。
|
||||
2. `POST /api/swarms` 会在极简 body 或 `dry_run:true` 情况下创建真实 swarm,缺少参数校验和 dry-run 保护。
|
||||
3. `/api/swarms` 返回的 `swarm_id` 当前无法通过 `/api/swarms/{swarm_id}` 查询,也无法通过 `/api/swarms/{swarm_id}/stop` 停止。
|
||||
4. `/api/agnet/deployments/{swarm_id}/stop` 也无法停止 `/api/swarms` 创建出的 swarm,返回 `DEPLOYMENT_NOT_FOUND`。
|
||||
2. Manager 生产已切到 `/api/swarms` 创建入口,并验证能拿到 `runtime_swarm_id=swm_*`。
|
||||
3. Agent Manager 自动 callback 已能写入 Manager `events/timeline`,Manager `1.4.19` 开始会把 callback 反写到 deployment 快照。
|
||||
4. 当前仍缺真实产物和真实用量:Agent Manager status 接口返回 `tokens_used=0`、`artifacts=[]`,并出现整体 `completed` 但 agent 仍 `running` 的状态不一致。
|
||||
5. 文档写了 callback 重试和死信,但同时又写“发送失败只 warning”,需要明确当前真实实现。
|
||||
6. 文档还缺少 Agent 运行 usage / cost 回传字段,后续无法做 NewAPI / Manager 账单归属和审计。
|
||||
|
||||
Heicode Manager 侧已确认:
|
||||
|
||||
- `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 生产路由在线。
|
||||
- callback 使用 `X-Agnet-Service-Token` 认证可以进入业务校验。
|
||||
- 不带认证会返回 `CALLBACK_UNAUTHORIZED`。
|
||||
- 带认证但缺少事件字段会返回 `CALLBACK_INVALID`。
|
||||
- `GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema` 当前生产返回 404,Manager 会单独复核上线/路由;Agent Manager 不应依赖这个接口才能推送 callback。
|
||||
- callback 支持 HMAC 和旧 token 兼容;当前生产为了先跑通自动 callback,已配置 HMAC fallback。
|
||||
- `GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema` 已由 Manager 提供,用于联调前核对事件类型和必填字段。
|
||||
- Manager 可接收 `deployment.status_changed`、`phase.changed`、`timeline.updated`、`agent.started/completed/crashed`,并在 `1.4.19` 反写 deployment 详情状态。
|
||||
|
||||
## 2. Agent Manager 必须整改的 P0
|
||||
|
||||
| 优先级 | 事项 | 当前问题 | 要求 |
|
||||
|---|---|---|---|
|
||||
| P0 | 固定联调地址 | 当前阶段约定先走 IP,后续再切域名 | 文档和配置先统一使用 `http://20.212.121.126`;域名切换另行确认 |
|
||||
| P0 | `/api/swarms` 参数校验 | 极简 body / `dry_run:true` 也创建真实 swarm | 无有效 `orchestration_plan`、`callback.url`、`sub_mode` 时返回 4xx;支持 `dry_run` 则不能创建真实资源,不支持则返回 422 |
|
||||
| P0 | 返回可操作 ID | `/api/swarms` 只返回 `swarm_id`,但 stop/detail 不认这个 ID | 创建响应必须返回 `deployment_id` 和 `swarm_id`,且至少一个 ID 可用于查询和停止 |
|
||||
| P0 | Swarm detail | `/api/swarms/{swarm_id}` 当前 404 | 实现详情接口,返回当前状态、agents、phase、created_at、updated_at |
|
||||
| P0 | Swarm stop | `/api/swarms/{swarm_id}/stop` 当前 404 | 实现停止接口,保证 Manager 停止任务时 Runtime 真实停止 |
|
||||
| P0 | callback 主动推送 | 文档说已支持,但还没完成 Manager 端真实闭环验证 | 创建 Runtime run 后主动向 Manager 推送 phase/timeline/artifact/tool/status callback |
|
||||
| P0 | usage 回传 | 缺少真实 usage / cost 字段 | 通过 callback 或事件接口回传 token、成本、运行时长、资源使用 |
|
||||
| P0 | 创建普通 sub deployment | `/api/swarms` 已能创建并返回 `swm_*` | 保持幂等和字段稳定,后续域名切换不能破坏 |
|
||||
| P0 | deployment detail | Manager 已能记录 callback;Agent Manager status 仍有 `completed` 与 agent `running` 不一致 | 返回真实 phase、agent 状态、失败原因和更新时间 |
|
||||
| P0 | deployment stop | `/api/agnet/deployments/{deployment_id}/stop` 已可用 | 停止后 Runtime 真实停止,并回调 stopped 事件 |
|
||||
| P0 | callback 主动推送 | 已验证 status/phase/timeline/agent/budget callback 能进入 Manager | 继续补 artifact、SK、approval 的真实运行数据 |
|
||||
| P0 | usage 回传 | 当前 budget callback 中 token/cost/runtime 多为 0 | 通过 callback 或事件接口回传真实 token、成本、运行时长、资源使用 |
|
||||
|
||||
## 3. 推荐接口契约
|
||||
|
||||
@@ -53,10 +50,10 @@ GET /api/agnet/health
|
||||
|
||||
### 3.2 创建普通 sub 运行
|
||||
|
||||
Heicode Manager 可以配置 Runtime create path 为 `/api/swarms` 或 `/api/agnet/deployments`。如果 Agent Manager 推荐 `/api/swarms`,则必须支持下面契约。
|
||||
普通 sub 当前生产联调使用 Agent Manager `/api/agnet/deployments`,蜂群 `/api/swarms` 另见 `AgentManager蜂群Runtime接口实现要求.md`。
|
||||
|
||||
```http
|
||||
POST /api/swarms
|
||||
POST /api/agnet/deployments
|
||||
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
Content-Type: application/json
|
||||
X-User-ID: <user_id>
|
||||
@@ -142,11 +139,10 @@ X-Idempotency-Key: <idempotency_key>
|
||||
```json
|
||||
{
|
||||
"deployment_id": "dep_runtime_123",
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"status": "initializing",
|
||||
"agents": [
|
||||
"agent_instances": [
|
||||
{
|
||||
"agent_id": "agi_backend_001",
|
||||
"agent_instance_id": "agi_backend_001",
|
||||
"role": "backend",
|
||||
"status": "pending"
|
||||
}
|
||||
@@ -159,16 +155,13 @@ X-Idempotency-Key: <idempotency_key>
|
||||
要求:
|
||||
|
||||
1. `deployment_id` 必须可用于 `/api/agnet/deployments/{deployment_id}`、`/api/agnet/deployments/{deployment_id}/stop`。
|
||||
2. `swarm_id` 必须可用于 `/api/swarms/{swarm_id}`、`/api/swarms/{swarm_id}/stop`。
|
||||
3. 如果只保留一种 ID,必须在文档中明确 Manager 应该配置哪个 stop path。
|
||||
4. 同一个 `X-Idempotency-Key` 重复请求必须返回同一个运行,不应重复创建。
|
||||
5. 请求缺少 `orchestration_plan`、`callback.url`、`sub_mode`、`user_context` 时必须返回 4xx。
|
||||
6. 不支持 `dry_run` 时,包含 `dry_run:true` 的请求必须返回 422,不能创建真实运行。
|
||||
2. 同一个 `X-Idempotency-Key` 重复请求必须返回同一个运行,不应重复创建。
|
||||
3. 请求缺少 `orchestration_plan`、`callback.url`、`sub_mode`、`user_context` 时必须返回 4xx。
|
||||
|
||||
### 3.3 查询 Swarm
|
||||
### 3.3 查询普通 sub deployment
|
||||
|
||||
```http
|
||||
GET /api/swarms/{swarm_id}
|
||||
GET /api/agnet/deployments/{deployment_id}
|
||||
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
```
|
||||
|
||||
@@ -176,13 +169,12 @@ Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
|
||||
```json
|
||||
{
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"deployment_id": "dep_runtime_123",
|
||||
"status": "running",
|
||||
"phase": "development",
|
||||
"agents": [
|
||||
"agent_instances": [
|
||||
{
|
||||
"agent_id": "agi_backend_001",
|
||||
"agent_instance_id": "agi_backend_001",
|
||||
"role": "backend",
|
||||
"status": "running",
|
||||
"current_task": "实现接口"
|
||||
@@ -193,10 +185,10 @@ Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 停止 Swarm
|
||||
### 3.4 停止普通 sub deployment
|
||||
|
||||
```http
|
||||
POST /api/swarms/{swarm_id}/stop
|
||||
POST /api/agnet/deployments/{deployment_id}/stop
|
||||
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -214,7 +206,6 @@ Content-Type: application/json
|
||||
|
||||
```json
|
||||
{
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"deployment_id": "dep_runtime_123",
|
||||
"status": "stopped",
|
||||
"stopped_at": "2026-05-28T07:40:00Z"
|
||||
@@ -227,16 +218,6 @@ Content-Type: application/json
|
||||
- 停止后可以继续查询状态。
|
||||
- 停止成功后建议 callback 一条 `deployment.status_changed`,状态为 `stopped`。
|
||||
|
||||
### 3.5 停止 Deployment
|
||||
|
||||
如果 Agent Manager 选择 `/api/agnet/deployments/{deployment_id}/stop` 作为主停止接口,也必须保证 `/api/swarms` 创建响应里的 `deployment_id` 能用于这个接口。
|
||||
|
||||
```http
|
||||
POST /api/agnet/deployments/{deployment_id}/stop
|
||||
```
|
||||
|
||||
当前实测问题:使用 `/api/swarms` 返回的 `swarm_id` 调该接口返回 `DEPLOYMENT_NOT_FOUND`。需要修复 ID 映射或在响应中返回真实 `deployment_id`。
|
||||
|
||||
## 4. Callback 要求
|
||||
|
||||
Manager 生产 callback 地址:
|
||||
@@ -296,7 +277,6 @@ X-Agnet-Service-Token: <HEICODE_SERVICE_TOKEN>
|
||||
"event_id": "evt_phase_001",
|
||||
"event_type": "phase.changed",
|
||||
"deployment_id": "dep_manager_123",
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"agent_instance_id": "agi_backend_001",
|
||||
"occurred_at": "2026-05-28T07:35:00Z",
|
||||
"correlation_id": "corr_123",
|
||||
@@ -317,7 +297,6 @@ artifact 示例:
|
||||
"event_id": "evt_artifact_001",
|
||||
"event_type": "artifact.created",
|
||||
"deployment_id": "dep_manager_123",
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"agent_instance_id": "agi_backend_001",
|
||||
"occurred_at": "2026-05-28T07:38:00Z",
|
||||
"correlation_id": "corr_123",
|
||||
@@ -371,7 +350,6 @@ Agent Manager 文档当前缺少真实 usage 字段。普通 sub 后续要支持
|
||||
"event_id": "evt_usage_001",
|
||||
"event_type": "budget.alert",
|
||||
"deployment_id": "dep_manager_123",
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"agent_instance_id": "agi_backend_001",
|
||||
"occurred_at": "2026-05-28T07:39:00Z",
|
||||
"correlation_id": "corr_123",
|
||||
@@ -419,7 +397,7 @@ Manager 端已有 approval、credential lease、approve/reject 和 Runtime decis
|
||||
推荐接口:
|
||||
|
||||
```http
|
||||
POST /api/swarms/{swarm_id}/approvals/{approval_id}
|
||||
POST /api/agnet/deployments/{deployment_id}/approvals/{approval_id}
|
||||
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -432,7 +410,6 @@ Content-Type: application/json
|
||||
"decision": "approved",
|
||||
"manager_deployment_id": "dep_manager_123",
|
||||
"runtime_deployment_id": "dep_runtime_123",
|
||||
"swarm_id": "swm_runtime_123",
|
||||
"operation": "git.write",
|
||||
"resource_type": "git",
|
||||
"resource_id": "repo_main",
|
||||
@@ -478,9 +455,9 @@ Agent Manager 改完后,按下面步骤验收。
|
||||
| 步骤 | 操作 | 通过标准 |
|
||||
|---:|---|---|
|
||||
| 1 | `GET /api/agnet/health` | 返回 healthy |
|
||||
| 2 | `POST /api/swarms` 创建普通 sub run | 返回 `deployment_id`、`swarm_id`、`status` |
|
||||
| 2 | `POST /api/agnet/deployments` 创建普通 sub run | 返回 `deployment_id`、`status` |
|
||||
| 3 | 重复同一个 `X-Idempotency-Key` 创建 | 不重复创建,返回同一个 ID |
|
||||
| 4 | `GET /api/swarms/{swarm_id}` | 返回真实状态和 agents |
|
||||
| 4 | `GET /api/agnet/deployments/{deployment_id}` | 返回真实状态和 agents |
|
||||
| 5 | Runtime 主动 callback `phase.changed` | Manager callback 返回 success |
|
||||
| 6 | Runtime 主动 callback `timeline.updated` | Manager timeline 可查到 |
|
||||
| 7 | Runtime 主动 callback `artifact.created` | Manager artifacts 可查到 |
|
||||
@@ -488,7 +465,7 @@ Agent Manager 改完后,按下面步骤验收。
|
||||
| 9 | Runtime 主动 callback usage | Manager 能看到 usage 摘要或 callback event |
|
||||
| 10 | Runtime 主动 callback `approval.requested` | Manager pending approval 生成 |
|
||||
| 11 | Manager approve/reject | Agent Manager 收到 decision |
|
||||
| 12 | `POST /api/swarms/{swarm_id}/stop` | Runtime 停止,状态变为 stopped |
|
||||
| 12 | `POST /api/agnet/deployments/{deployment_id}/stop` | Runtime 停止,状态变为 stopped |
|
||||
| 13 | 检查日志和 artifact metadata | 不包含明文密钥 |
|
||||
|
||||
## 9. 当前实测记录
|
||||
@@ -502,10 +479,7 @@ Agent Manager 改完后,按下面步骤验收。
|
||||
| `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 无认证 | 业务返回 `CALLBACK_UNAUTHORIZED` | Manager callback 路由在线 |
|
||||
| `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 带旧 token 但空 body | 业务返回 `CALLBACK_INVALID` | 认证通过,进入事件校验 |
|
||||
| `GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema` | 404 | Manager schema GET 生产未通,Manager 侧需复核 |
|
||||
| `POST http://20.212.121.126/api/swarms` 带 `dry_run:true` | 200,返回 `swm_080c59b10bbe` | 对方实际创建了 swarm,需要修复参数校验/dry-run |
|
||||
| `GET http://20.212.121.126/api/swarms/swm_080c59b10bbe` | 404 | 缺少 swarm detail |
|
||||
| `POST http://20.212.121.126/api/swarms/swm_080c59b10bbe/stop` | 404 | 缺少 swarm stop |
|
||||
| `POST http://20.212.121.126/api/agnet/deployments/swm_080c59b10bbe/stop` | 404 / `DEPLOYMENT_NOT_FOUND` | `/api/swarms` 返回 ID 与 deployment stop 不兼容 |
|
||||
| `POST http://20.212.121.126/api/agnet/deployments` | 200 | 普通 sub Runtime deployment 创建可用 |
|
||||
|
||||
### 9.1 2026-05-28 追加联调记录
|
||||
|
||||
@@ -554,7 +528,7 @@ Heicode Manager callback 接收端:
|
||||
```text
|
||||
event_id=evt_agent_manager_ip_smoke_1779954828884
|
||||
event_type=timeline.updated
|
||||
swarm_id=dep_cd170573cf47
|
||||
runtime_deployment_id=dep_cd170573cf47
|
||||
source=agent-manager-ip-smoke
|
||||
```
|
||||
|
||||
@@ -563,8 +537,7 @@ source=agent-manager-ip-smoke
|
||||
1. Heicode Manager callback 接收端可用,旧 token 认证和幂等可用。
|
||||
2. Agent Manager IP 的 `/api/agnet/deployments` 创建、查询、停止可用。
|
||||
3. Agent Manager 当前没有在实测中产生普通 sub 所需的 phase/timeline/artifact/SK 主动回调。
|
||||
4. `/api/swarms` 仍存在 ID 不可查询/不可停止和 `dry_run` 误创建问题。
|
||||
5. 因没有可用生产 Manager 用户 session,本次没有完成“客户端登录态 -> Manager 用户态 deployment -> Runtime”的完整端到端测试。
|
||||
4. 因没有可用生产 Manager 用户 session,本次没有完成“客户端登录态 -> Manager 用户态 deployment -> Runtime”的完整端到端测试。
|
||||
|
||||
### 9.2 2026-05-28 测试用户客户端模拟链路
|
||||
|
||||
@@ -627,7 +600,7 @@ Agent Manager 侧直查 `dep_3335e54e9bdf`:
|
||||
|
||||
只有满足以下条件,才能认为普通 sub 敏捷 Runtime 联调完成:
|
||||
|
||||
1. Manager 能创建 Runtime run,并保存 Runtime 返回的 `deployment_id` / `swarm_id`。
|
||||
1. Manager 能创建 Runtime run,并保存 Runtime 返回的 `deployment_id`。
|
||||
2. Runtime 能主动回调 Manager,且 callback 通过认证、幂等、脱敏和 schema 校验。
|
||||
3. Manager 的 artifacts / timeline / sk-snapshots / events 能看到 Runtime 回传数据。
|
||||
4. Manager 发起 stop 后 Runtime 真实停止。
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,按 `sub_mode=agile` 和 `agile_context` 执行阶段化开发,持续把阶段状态、日志、事件、artifact、审批请求、用量和最终结果回传给 Manager。
|
||||
|
||||
计费边界:PayPal 只作为 Heicode Manager 未来收款渠道,用于余额充值或购买 Heicode 内部订阅套餐;普通 sub 任务执行中的模型费用仍按 Manager/NewAPI 的钱包余额或订阅额度扣费。Agent Manager 不需要接 PayPal,但必须回传模型用量和运行 usage,方便 Manager/NewAPI 做归属和审计。
|
||||
|
||||
普通 sub 不等于蜂群模式:
|
||||
|
||||
| 项 | 普通 sub 敏捷 | 蜂群模式 |
|
||||
@@ -34,7 +36,9 @@ Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,
|
||||
| artifact 查询 | 已完成 | `GET /api/agnet/user/deployments/{deployment_id}/artifacts` |
|
||||
| approval 查询/approve/reject | 已完成 | `/api/agnet/approvals` |
|
||||
| V2 body 加密 | 已完成 | 桌面客户端到 Manager 的 POST 请求复用模型调用加密协议 |
|
||||
| 生产验证 | 已完成 | Manager 1.4.9 已上线,公开 health 和页面点击通过 |
|
||||
| 生产验证 | 已完成核心链路 | 生产已验证 Manager `1.4.18` 可通过 `/api/swarms` 创建普通 sub run,持久化 `runtime_swarm_id=swm_*`,Agent Manager 自动 callback 可写入 `events/timeline`;`1.4.19` 补齐 callback 后反写 deployment 快照 |
|
||||
| callback 状态反写 | 已完成 | Manager 接收 `deployment.status_changed`、`phase.changed`、`timeline.updated`、`agent.started/completed/crashed` 后,会同步更新 deployment `status/phase/runtime_state/agent_instances`,避免详情页长期停留 `initializing/pending` |
|
||||
| PayPal/计费边界文档 | 已完成 | `docs/integration/Heicode-Manager-PayPal支付接入与计费关系说明.md` 已明确收款、余额、订阅、NewAPI 扣费和 Agent 运行预算关系 |
|
||||
|
||||
## 3. Agent Manager 需要实现或确认的 P0
|
||||
|
||||
@@ -49,6 +53,7 @@ Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,
|
||||
| 支持 stop | 是 | 用户停止任务时 Runtime 必须停止真实执行 | stop 接口返回 stopped |
|
||||
| 支持日志/事件/指标 | 是 | 联调和验收需要排障数据 | 提供 callback 或查询接口 |
|
||||
| 支持用量归属 | 是 | NewAPI / CodeGW 计费要按 deployment/task/role 归属 | 回传 usage/budget 事件或在模型调用中带 correlation |
|
||||
| 支持 Runtime usage 回传 | 是 | `budget.max_cost_usd` 只是预算上限,真实 Agent 运行费用不能由 Manager 猜 | 回传模型 token/cost、runtime 秒数、CPU/内存等可核对 usage |
|
||||
|
||||
## 4. Runtime 创建接口
|
||||
|
||||
@@ -416,6 +421,8 @@ Agent Manager 至少需要支持以下一种方式:
|
||||
|
||||
Agent Manager 调模型时,需要把 Manager 传入的上下文带上,便于 NewAPI / CodeGW 计费归属。
|
||||
|
||||
PayPal 不在这条链路里。PayPal 支付成功后只会把用户权益写入 Heicode 钱包余额或内部订阅;普通 sub 执行时,Agent Manager 仍然按 `billing_context.provider=newapi` 使用 Manager 传入的用户、deployment、role 和模型上下文。
|
||||
|
||||
至少保留:
|
||||
|
||||
| 字段 | 来源 | 用途 |
|
||||
@@ -427,6 +434,19 @@ Agent Manager 调模型时,需要把 Manager 传入的上下文带上,便于
|
||||
| `task_id` | Runtime 内部任务 | 子任务归属 |
|
||||
| `model_id` | billing/default model | 模型用量 |
|
||||
|
||||
Agent Manager 回传 usage 时建议至少包含:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `model_tokens` | 本 deployment/task/role 消耗的模型 token。 |
|
||||
| `model_cost_usd` | Runtime 或 NewAPI 可确认的模型成本。 |
|
||||
| `runtime_seconds` | Agent 实际运行秒数。 |
|
||||
| `cpu_core_seconds` | 如 Runtime 具备集群指标,应回传 CPU 使用量。 |
|
||||
| `memory_mb_seconds` | 如 Runtime 具备集群指标,应回传内存使用量。 |
|
||||
| `billing_source` | 建议为 `newapi`、`manager_wallet`、`manager_subscription` 或 Runtime 约定值。 |
|
||||
|
||||
注意:`budget.max_tokens`、`budget.max_cost_usd`、`budget.max_duration_sec` 是预算和拦截上限,不是已结算金额。Manager 只能展示和审计预算;真实扣费或成本归属必须基于 Agent Manager / Runtime 回传的 usage。
|
||||
|
||||
如果发生预算告警,回调:
|
||||
|
||||
```json
|
||||
@@ -467,7 +487,9 @@ Agent Manager 调模型时,需要把 Manager 传入的上下文带上,便于
|
||||
| 7 | Manager approve/reject | Runtime 收到 decision 并继续/停止对应动作 |
|
||||
| 8 | Manager stop | Runtime deployment 真实停止 |
|
||||
| 9 | Runtime 回调 completed/failed | Manager detail/timeline 显示终态 |
|
||||
| 10 | 查日志 | 不出现明文密钥,不丢 `correlation_id` |
|
||||
| 10 | Runtime 回调用量 | Manager 能看到 model token/cost 或 usage 摘要,且与 deployment/task/role/correlation 关联 |
|
||||
| 11 | 查日志 | 不出现明文密钥,不丢 `correlation_id` |
|
||||
| 12 | 生产路由复核 | `GET /api/agnet/callbacks/swarm-events/schema` 返回 200,用户态/后台态接口使用有效登录态通过 smoke |
|
||||
|
||||
## 13. Agent Manager 不需要处理的内容
|
||||
|
||||
@@ -478,6 +500,7 @@ Agent Manager 调模型时,需要把 Manager 传入的上下文带上,便于
|
||||
| HeicodeTask 追问/任务卡生成 | 客户端和 Manager 代理处理 |
|
||||
| Azure Key Vault 写入长期密钥 | Manager 负责资源绑定和 `secret_ref` 管理 |
|
||||
| Web 控制台页面展示 | Manager 负责 |
|
||||
| PayPal 收款和充值订单 | Manager 负责;Agent Manager 只需要回传执行和用量,不直接处理支付 |
|
||||
|
||||
## 14. 当前已知缺口
|
||||
|
||||
@@ -488,4 +511,6 @@ Agent Manager 调模型时,需要把 Manager 传入的上下文带上,便于
|
||||
| artifact 真实产出 | Agent Manager | 需要 Runtime 输出 Git/存储/报告 URI |
|
||||
| approval decision 接收路径 | Agent Manager + Manager 配置 | 需确认最终路径是 `/api/swarms/...` 还是 `/api/agnet/deployments/...` |
|
||||
| 用量回传 | Agent Manager + NewAPI/CodeGW | 需要按 user/deployment/task/role 归属 |
|
||||
| Agent 运行费用真实结算 | Agent Manager + Manager | 当前 Manager 有预算字段和文档口径,缺 Runtime 真实 usage,不能只按预算估算扣费 |
|
||||
| 生产 callback schema 路由 | Manager 部署/路由 | 本地代码和测试已覆盖,2026-05-28 生产公开访问 `/api/agnet/callbacks/swarm-events/schema` 返回 404,需要重新上线或核对生产镜像/路由 |
|
||||
| 无 body GET 的 V2 签名 | 客户端 + Manager | 这是客户端全链路无 cookie 的后续项,不阻塞 Agent Manager 创建/回调联调 |
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.4.18
|
||||
1.4.19
|
||||
|
||||
@@ -230,6 +230,124 @@ func callbackMapValue(values map[string]any, key string) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func agnetCallbackEventRuntimeState(eventType string, payload map[string]any) string {
|
||||
if state := callbackStringValue(payload, "status"); state != "" {
|
||||
return state
|
||||
}
|
||||
switch eventType {
|
||||
case "agent.started":
|
||||
return "running"
|
||||
case "agent.completed":
|
||||
return "completed"
|
||||
case "agent.crashed", "sk_tool.failed", "task.failed":
|
||||
return "failed"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agnetCallbackEnvelope, phase string, runtimeState string) bool {
|
||||
if record == nil {
|
||||
return false
|
||||
}
|
||||
instanceID := strings.TrimSpace(payload.AgentInstanceID)
|
||||
source := payload.Payload
|
||||
role := firstNonEmpty(callbackStringValue(source, "agent_role"), callbackStringValue(source, "role"))
|
||||
if role == "" && instanceID == "" {
|
||||
return false
|
||||
}
|
||||
failureReason := firstNonEmpty(callbackStringValue(source, "reason"), callbackStringValue(source, "error"))
|
||||
for idx := range record.AgentInstances {
|
||||
instance := &record.AgentInstances[idx]
|
||||
if instanceID != "" && strings.TrimSpace(instance.InstanceID) == instanceID {
|
||||
if role != "" {
|
||||
instance.Role = role
|
||||
}
|
||||
if phase != "" {
|
||||
instance.Phase = phase
|
||||
}
|
||||
if runtimeState != "" {
|
||||
instance.RuntimeState = runtimeState
|
||||
}
|
||||
if failureReason != "" {
|
||||
instance.FailureReason = failureReason
|
||||
}
|
||||
return true
|
||||
}
|
||||
if instanceID != "" && role != "" && strings.TrimSpace(instance.Role) == role && (strings.TrimSpace(instance.RuntimeState) == "queued" || strings.TrimSpace(instance.RuntimeState) == "pending") {
|
||||
instance.InstanceID = instanceID
|
||||
instance.Phase = firstNonEmpty(phase, instance.Phase)
|
||||
instance.RuntimeState = firstNonEmpty(runtimeState, instance.RuntimeState)
|
||||
instance.FailureReason = failureReason
|
||||
return true
|
||||
}
|
||||
}
|
||||
record.AgentInstances = append(record.AgentInstances, agnetAgentInstance{
|
||||
InstanceID: firstNonEmpty(instanceID, "agi_"+common.GetUUID()[:12]),
|
||||
Role: role,
|
||||
Phase: firstNonEmpty(phase, record.Phase),
|
||||
RuntimeState: firstNonEmpty(runtimeState, record.RuntimeState),
|
||||
FailureReason: failureReason,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
func applyAgnetCallbackDeploymentState(payload agnetCallbackEnvelope, record agnetDeploymentRecord) (agnetDeploymentRecord, bool) {
|
||||
if strings.TrimSpace(record.DeploymentID) == "" {
|
||||
return record, false
|
||||
}
|
||||
source := payload.Payload
|
||||
changed := false
|
||||
switch payload.EventType {
|
||||
case "deployment.status_changed":
|
||||
status := callbackStringValue(source, "status")
|
||||
if status != "" {
|
||||
record.Status = status
|
||||
record.RuntimeState = status
|
||||
changed = true
|
||||
}
|
||||
case "phase.changed":
|
||||
phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"))
|
||||
checkpoint := callbackStringValue(source, "checkpoint")
|
||||
if phase != "" {
|
||||
record.Phase = phase
|
||||
changed = true
|
||||
}
|
||||
if checkpoint != "" {
|
||||
record.RuntimeState = checkpoint
|
||||
changed = true
|
||||
}
|
||||
case "timeline.updated":
|
||||
phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"))
|
||||
checkpoint := callbackStringValue(source, "checkpoint")
|
||||
if phase != "" {
|
||||
record.Phase = phase
|
||||
changed = true
|
||||
}
|
||||
if checkpoint != "" && !containsString([]string{"completed", "failed", "stopped"}, record.RuntimeState) {
|
||||
record.RuntimeState = checkpoint
|
||||
changed = true
|
||||
}
|
||||
case "agent.started", "agent.completed", "agent.crashed":
|
||||
phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"), record.Phase)
|
||||
runtimeState := agnetCallbackEventRuntimeState(payload.EventType, source)
|
||||
if upsertAgnetCallbackAgentInstance(&record, payload, phase, runtimeState) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return record, false
|
||||
}
|
||||
record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agnetNow())
|
||||
agnetMu.Lock()
|
||||
agnetDeployments[record.DeploymentID] = record
|
||||
agnetMu.Unlock()
|
||||
if err := updateAgnetDeploymentRecord(record); err != nil {
|
||||
common.SysLog("applyAgnetCallbackDeploymentState: " + err.Error())
|
||||
}
|
||||
return record, true
|
||||
}
|
||||
|
||||
func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
|
||||
if payload == nil || strings.TrimSpace(payload.Artifact.ArtifactID) != "" {
|
||||
return
|
||||
@@ -601,6 +719,7 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
|
||||
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request")
|
||||
return
|
||||
}
|
||||
record, _ = applyAgnetCallbackDeploymentState(payload, record)
|
||||
recordAgnetAuditEvent(agnetEvent{
|
||||
EventID: "evt_" + common.GetUUID()[:12],
|
||||
Event: "callback." + payload.EventType,
|
||||
|
||||
@@ -1218,6 +1218,92 @@ func TestAgnetCallbackUsesRuntimeDeploymentIDFallback(t *testing.T) {
|
||||
require.Contains(t, stored.PlanJSON, `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/model-gateway-key"`)
|
||||
}
|
||||
|
||||
func TestAgnetCallbackUpdatesDeploymentRuntimeSnapshot(t *testing.T) {
|
||||
db := setupAgnetControlPlaneTestDB(t)
|
||||
resetAgnetControlPlaneState(t)
|
||||
t.Setenv("AGNET_CALLBACK_TOKEN", "callback-token")
|
||||
|
||||
plan := baseAgnetResourceGrantPlan()
|
||||
plan.UserContext.UserID = "7"
|
||||
for idx := range plan.Agents[0].ResourceGrants {
|
||||
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
||||
}
|
||||
createRecorder, createEnvelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||
require.True(t, createEnvelope.Success)
|
||||
var createBody map[string]any
|
||||
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||
runtimeID := "swm-runtime-state-1"
|
||||
require.NoError(t, db.Model(&model.AgnetDeployment{}).
|
||||
Where("deployment_id = ?", deploymentID).
|
||||
Updates(map[string]any{
|
||||
"runtime_deployment_id": runtimeID,
|
||||
"runtime_swarm_id": runtimeID,
|
||||
}).Error)
|
||||
resetAgnetControlPlaneState(t)
|
||||
|
||||
postCallback := func(body string) {
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/callbacks/swarm-events", strings.NewReader(body))
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
ctx.Request.Header.Set("X-Agnet-Service-Token", "callback-token")
|
||||
AgnetReceiveSwarmEventCallback(ctx)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), `"success":true`)
|
||||
}
|
||||
|
||||
postCallback(`{
|
||||
"event_id":"evt-runtime-agent-started",
|
||||
"event_type":"agent.started",
|
||||
"deployment_id":"swm-runtime-state-1",
|
||||
"swarm_id":"swm-runtime-state-1",
|
||||
"agent_instance_id":"agi-runtime-builder",
|
||||
"occurred_at":"2026-05-28T12:40:00Z",
|
||||
"payload":{"agent_role":"builder","status":"running"}
|
||||
}`)
|
||||
postCallback(`{
|
||||
"event_id":"evt-runtime-phase-deploy",
|
||||
"event_type":"phase.changed",
|
||||
"deployment_id":"swm-runtime-state-1",
|
||||
"swarm_id":"swm-runtime-state-1",
|
||||
"occurred_at":"2026-05-28T12:41:00Z",
|
||||
"payload":{"stage":"deploy","checkpoint":"completed","summary":"Runtime completed"}
|
||||
}`)
|
||||
postCallback(`{
|
||||
"event_id":"evt-runtime-agent-completed",
|
||||
"event_type":"agent.completed",
|
||||
"deployment_id":"swm-runtime-state-1",
|
||||
"swarm_id":"swm-runtime-state-1",
|
||||
"agent_instance_id":"agi-runtime-builder",
|
||||
"occurred_at":"2026-05-28T12:41:30Z",
|
||||
"payload":{"agent_role":"builder","status":"completed","stage":"deploy"}
|
||||
}`)
|
||||
postCallback(`{
|
||||
"event_id":"evt-runtime-status-completed",
|
||||
"event_type":"deployment.status_changed",
|
||||
"deployment_id":"swm-runtime-state-1",
|
||||
"swarm_id":"swm-runtime-state-1",
|
||||
"occurred_at":"2026-05-28T12:42:00Z",
|
||||
"payload":{"status":"completed"}
|
||||
}`)
|
||||
|
||||
var stored model.AgnetDeployment
|
||||
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
||||
require.Equal(t, "completed", stored.Status)
|
||||
require.Equal(t, "deploy", stored.Phase)
|
||||
require.Equal(t, "completed", stored.RuntimeState)
|
||||
require.Equal(t, "2026-05-28T12:42:00Z", stored.UpdatedAtText)
|
||||
|
||||
var instances []agnetAgentInstance
|
||||
require.NoError(t, common.UnmarshalJsonStr(stored.AgentInstancesJSON, &instances))
|
||||
require.Len(t, instances, 1)
|
||||
require.Equal(t, "agi-runtime-builder", instances[0].InstanceID)
|
||||
require.Equal(t, "builder", instances[0].Role)
|
||||
require.Equal(t, "deploy", instances[0].Phase)
|
||||
require.Equal(t, "completed", instances[0].RuntimeState)
|
||||
}
|
||||
|
||||
func TestAgnetCallbackRejectsPlaintextSecretsAndMissingToken(t *testing.T) {
|
||||
setupAgnetControlPlaneTestDB(t)
|
||||
resetAgnetControlPlaneState(t)
|
||||
|
||||
Reference in New Issue
Block a user