Files
heicode-mananger/heicode/model/agent_callback.go
T
chenchenandClaude Opus 4.8 56a9b02a25 feat(swarm): adopt frozen agent_swarm contract v1 (read-side) (#45/#46)
按 agent_swarm#14/#15 冻结契约做 HM 读侧适配(客户端 #28 消费):
- 注册 6 类新事件(event-schema v1):swarm.completed/failed/stopped、approval.approved/rejected、
  handoff.created(categories + requiredFields 两表;必填先最小集,待 agent_swarm PR #28 §4 精校)。
- AgentCallbackEvent 增 Sequence(per-swarm 严格递增序号),回调入库透传 envelope.sequence;
  事件视图暴露 sequence 供客户端去重/排序。游标仍用稳定 id(next_after)避免 sequence 未全量
  上线时回归。
- 脱敏键补 credential_ref/signing_secret_ref(envelope 按设计透传 azkv:// 引用,客户端视图剔除)。
- artifact.created → 扁平视图 {uri,checksum,task_id,size_bytes?,created_at}(无 secret_ref;
  size 未知省略不伪造)。
- 状态展示映射 §4.1:display_status(blocked→degraded;不臆造 preparing/verifying)。

测试:状态映射、artifact 视图(脱敏 + size 省略)、6 类事件注册;controller+model 全回归通过,
go build/vet 干净。文档 §5.2 更新。

未含(下一 PR):stop 真实运行时接入(写路径,复用 agentRuntimeClientConfigForMode("swarm"))。

Affects: Manager only(只读查询契约适配)。AgentCallbackEvent 加列(AutoMigrate);无计费改动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:01:32 +08:00

121 lines
4.2 KiB
Go

package model
import (
"errors"
"strings"
)
type AgentCallbackEvent struct {
Id int `gorm:"primaryKey" json:"id"`
EventID string `gorm:"type:varchar(128);uniqueIndex" json:"event_id"`
IdempotencyKey string `gorm:"type:varchar(128);index" json:"idempotency_key"`
CallbackType string `gorm:"type:varchar(64);index" json:"callback_type"`
EventType string `gorm:"type:varchar(64);index" json:"event_type"`
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
SwarmID string `gorm:"type:varchar(128);index" json:"swarm_id"`
AgentInstanceID string `gorm:"type:varchar(128);index" json:"agent_instance_id"`
TaskID string `gorm:"type:varchar(128);index" json:"task_id"`
UserID string `gorm:"type:varchar(64);index" json:"user_id"`
BindingScope string `gorm:"type:varchar(512);index" json:"binding_scope"`
CorrelationID string `gorm:"type:varchar(64);index" json:"correlation_id"`
// agent_swarm event-schema v1(#15):per-swarm 严格递增序号(每 swarm 从 1、无空洞)。
// 0 = envelope 未带(legacy / 非 swarm);对外作客户端续传/去重游标。
Sequence int `gorm:"index;default:0" json:"sequence"`
Source string `gorm:"type:varchar(64)" json:"source"`
Result string `gorm:"type:varchar(32)" json:"result"`
PayloadJSON string `gorm:"type:text" json:"payload_json"`
OccurredAt string `gorm:"type:varchar(32)" json:"occurred_at"`
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
}
func (AgentCallbackEvent) TableName() string {
return "agent_callback_events"
}
type ListAgentCallbackEventsFilter struct {
DeploymentID string
TaskID string
CorrelationID string
Limit int
}
func InsertAgentCallbackEvent(row *AgentCallbackEvent) (bool, error) {
if DB == nil || row == nil {
return false, nil
}
if row.EventID == "" {
return false, errors.New("event_id is required")
}
var existing AgentCallbackEvent
if err := DB.Where("event_id = ?", row.EventID).First(&existing).Error; err == nil {
return false, nil
}
if row.IdempotencyKey != "" {
if err := DB.Where("idempotency_key = ?", row.IdempotencyKey).First(&existing).Error; err == nil {
return false, nil
}
}
if err := DB.Create(row).Error; err != nil {
return false, err
}
return true, nil
}
// ListSwarmCallbackEventsAfter returns persisted swarm runtime events for one run
// (matched by deployment_id OR swarm_id) with an id-based `after` cursor for
// incremental polling (#45 events?after). Ordered oldest-first so the client can
// append; the caller uses the last returned Id as the next `after`. Reading from
// HM-persisted callback rows means this needs no live Swarm call.
func ListSwarmCallbackEventsAfter(userID, deploymentID, swarmID string, afterID, limit int) ([]AgentCallbackEvent, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgentCallbackEvent{})
switch {
case deploymentID != "" && swarmID != "":
q = q.Where("deployment_id = ? OR swarm_id = ?", deploymentID, swarmID)
case deploymentID != "":
q = q.Where("deployment_id = ?", deploymentID)
case swarmID != "":
q = q.Where("swarm_id = ?", swarmID)
default:
return nil, nil
}
// 防跨用户泄漏:即使 runtime_swarm_id/deployment_id 碰撞或误写,也按 user_id 收口(#45 复审 #3)。
if strings.TrimSpace(userID) != "" {
q = q.Where("user_id = ?", userID)
}
if afterID > 0 {
q = q.Where("id > ?", afterID)
}
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgentCallbackEvent
err := q.Order("id asc").Limit(limit).Find(&items).Error
return items, err
}
func ListAgentCallbackEvents(f ListAgentCallbackEventsFilter) ([]AgentCallbackEvent, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgentCallbackEvent{})
if f.DeploymentID != "" {
q = q.Where("deployment_id = ?", f.DeploymentID)
}
if f.TaskID != "" {
q = q.Where("task_id = ?", f.TaskID)
}
if f.CorrelationID != "" {
q = q.Where("correlation_id = ?", f.CorrelationID)
}
limit := f.Limit
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgentCallbackEvent
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}