Files
sundynix-agentix/sundynix-shared/contract/task.go
T

307 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package contract 是 Gateway / Dispatcher / MCP 之间的共享契约:
// Task 数据结构与 NATS subject 命名约定。
package contract
import "encoding/json"
// NATS subject / stream 约定(与 README、各服务 config 保持一致)。
const (
StreamTasks = "SUNDYNIX_TASKS" // JetStream stream 名
SubjectTasks = "sundynix.tasks" // 任务发布主题前缀;实际为 sundynix.tasks.<id>
SubjectTasksAll = "sundynix.tasks.>" // stream 捕获的通配
SubjectStream = "sundynix.streams" // Token 回流前缀;实际 sundynix.streams.<id>
ConsumerDurable = "dispatchers" // Dispatcher 持久消费者(队列组负载均衡)
// HeaderStreamEnd 是 Token 流的结束信号(core NATS 消息头)。
// 置为 "1" 的消息体为空,表示该 task 的 Token 流结束。
HeaderStreamEnd = "X-Stream-End"
// MCP 工具调用约定(第 4 层 Dispatcher → 第 5 层 MCP Tools)。
// 用 core NATS request-reply:同步拿结果,队列组内负载均衡。
SubjectToolsGo = "sundynix.tools.go" // Go I/O 型工具前缀;实际 sundynix.tools.go.<tool>
SubjectToolsGoAll = "sundynix.tools.go.>" // mcp-go 通配订阅
SubjectToolsPy = "sundynix.tools.py" // Python 算法型工具前缀;实际 sundynix.tools.py.<tool>
SubjectToolsPyAll = "sundynix.tools.py.>" // mcp-py 通配订阅
QueueToolsGo = "mcp-go-workers" // mcp-go 队列组(多副本负载均衡)
QueueToolsPy = "mcp-py-workers" // mcp-py 队列组
// 平台工具(JARVIS 大脑中枢,见 JARVIS_BRAIN_DESIGN.md):由 gateway 自己提供——
// 平台操作的权威(提交关卡/归属校验/计费)都在 gateway,工具就长在权威所在地。
SubjectToolsPlatform = "sundynix.tools.platform" // 前缀;实际 sundynix.tools.platform.<tool>
SubjectToolsPlatformAll = "sundynix.tools.platform.>" // gateway 通配订阅
QueueToolsPlatform = "platform-tools-workers" // gateway 多副本队列组
// 语音事件通道(JARVIS P2 动作/P3 主动播报):服务端任意副本 → 某用户的语音 WS 会话。
// core NATS pub-sub(即时性消息:navigate/播报,丢一条无伤;会话在哪个副本谁订阅谁收)。
SubjectVoiceEvent = "sundynix.voice.event" // 前缀;实际 sundynix.voice.event.<user_id>
// 本地执行路由(JARVIS P4「本地的手」,LOCAL_AGENT_DESIGN 档 A):local_* 工具调用
// 经此主题路由到「持有该用户桌面 runner WS 连接」的 gateway 副本(request-reply
// 载荷复用 ToolCall/ToolResult)。无人订阅 = 用户桌面端不在线 → 调用方明确报不可用。
SubjectLocalExec = "sundynix.local.exec" // 前缀;实际 sundynix.local.exec.<user_id>
// QueueGateway 是网关侧事件订阅/配置应答的队列组:多网关副本下,每条
// eval/usage/status 事件与每个 config 请求只由组内一个副本处理,避免重复落库/重复应答(HA)。
QueueGateway = "gateway-workers"
// 服务探活:dispatcher 既无 HTTP 端点也不挂工具,单独用一个 core NATS
// request-reply 心跳主题让控制面(管理端「服务状态」)能判定它在不在线。
SubjectHealthDispatcher = "sundynix.health.dispatcher"
// 任务生命周期状态回写:dispatcher 开跑/跑完/出错经此主题广播,网关订阅落 PG 并推 UI。
// core NATS pub-sub(状态是幂等覆盖,丢一条由下一条纠正,无需持久化)。
// 注意:必须在 sundynix.tasks.> 之外,否则会被任务流捕获成"幽灵任务"自我放大。
SubjectTaskStatus = "sundynix.status.task"
// 人工审批(HITL)决定回传前缀:实际 sundynix.approval.<task_id>。
// UI 点批准/拒绝 → 网关 nc.Publish 到此。阻塞模型经 core NATS WaitApproval 收;
// 中断/恢复模型经下方 JetStream 流持久捕获、由 dispatcher 持久消费者驱动 resume(抗离线)。
SubjectApproval = "sundynix.approval"
SubjectApprovalAll = "sundynix.approval.>" // 审批决定流捕获的通配
StreamApprovals = "SUNDYNIX_APPROVALS" // 审批决定 JetStream 流(持久,决定不因 dispatcher 离线而丢)
ConsumerApprovals = "approval-resumers" // 审批决定持久消费者(队列组:多副本下每条决定只一个副本处理 resume)
// 状态 / 用量回写升级为 JetStream 持久(此前 core NATS:网关离线/慢消费者会丢——尤其 usage 丢=漏账)。
// 落库幂等(task_id 唯一 + 门控),故 at-least-once 重投安全,不会重复扣费。subject 均在 tasks.> 之外。
StreamStatus = "SUNDYNIX_STATUS" // 任务状态回写流(持久,done/failed 不因网关离线而丢)
ConsumerStatus = "gateway-status" // 状态回写持久消费者(队列组:多网关副本每条只落一次)
StreamUsage = "SUNDYNIX_USAGE" // 用量回写流(持久,计费凭据不丢)
ConsumerUsage = "gateway-usage" // 用量回写持久消费者
StreamEval = "SUNDYNIX_EVAL" // 评测结果回写流(持久,网关离线不丢评测——SaveEval 按 task_id upsert 幂等)
ConsumerEval = "gateway-eval" // 评测回写持久消费者
// BucketCheckpoints 是 HITL 持久化中断的 JetStream KV 桶名:存 compose 图 checkpoint
// (键=task_id)与 resume 记录(键=pending:task_id),dispatcher 重启后可据此恢复在途审批。
BucketCheckpoints = "SUNDYNIX_CHECKPOINTS"
// 自动化评测结果回写:dispatcher 评完经此发到持久流,网关消费落 PG 供 UI 查询。
// 已升 JetStream(此前 core NATS:网关离线/慢消费者会丢评测);SaveEval upsert 幂等,重投安全。
SubjectEval = "sundynix.eval.task"
// Token 用量回写:dispatcher 任务收尾经此广播本轮 token 用量,网关订阅累加到用户日预算并供计费。core NATS pub-sub。
SubjectUsage = "sundynix.usage.task"
)
// UsageEvent 是一次任务的 token 用量(dispatcher 收尾经 SubjectUsage 回流给网关累计/计费)。
// dispatcher 不持有计价,只报原始 token + 模型名;真钱成本由网关据 Pricing 折算。
type UsageEvent struct {
TaskID string `json:"task_id"`
UserID string `json:"user_id,omitempty"`
TenantID string `json:"tenant_id,omitempty"` // 租户标识(按租户计量 / 计费)
Model string `json:"model,omitempty"`
PromptTok int `json:"prompt_tok"` // 输入 token(估算)
CompTok int `json:"comp_tok"` // 输出 token(估算)
TotalTok int `json:"total_tok"` // 合计
Exceeded bool `json:"exceeded"` // 是否触顶单任务预算被中止
TS int64 `json:"ts"` // unix 毫秒
}
// 评测质量分级(据综合分 + 忠实度阈值,闭环门控/告警用)。
const (
EvalOK = "ok" // 综合 ≥ 0.75 且无忠实度风险
EvalWarn = "warn" // 0.5 ≤ 综合 < 0.75,或忠实度偏低
EvalPoor = "poor" // 综合 < 0.5
)
// EvalEvent 是一次自动化评测的结果(经 SubjectEval 回流给网关落库)。
type EvalEvent struct {
TaskID string `json:"task_id"`
Overall float64 `json:"overall"` // 综合分 [0,1]
Rule float64 `json:"rule"` // 规则分
LLM float64 `json:"llm"` // LLM 质量分
Faithful float64 `json:"faithful"` // RAG 忠实度分(0=无来源未评)
Level string `json:"level"` // ok / warn / poor(纠偏后的终值)
Flags []string `json:"flags,omitempty"` // 命中问题(规则 + 未被来源支持)
Reason string `json:"reason,omitempty"` // 评语
Sources int `json:"sources,omitempty"` // 检索来源数
Corrected bool `json:"corrected,omitempty"` // 是否经低分自动纠偏重生成后采纳(恒温器闭环)
TS int64 `json:"ts"` // unix 毫秒
}
// 任务生命周期状态机:submitted(网关建任务)→ runningdispatcher 开跑)
// → done / failed / timeoutdispatcher 收尾)。
// HITL:执行到审批节点 → waiting(等人工决定)→ 批准回 running / 拒绝→rejected。
const (
TaskSubmitted = "submitted"
TaskRunning = "running"
TaskDone = "done"
TaskFailed = "failed"
TaskTimeout = "timeout"
TaskWaiting = "waiting" // 等待人工审批
TaskRejected = "rejected" // 人工拒绝(或审批超时,安全默认拒绝)
)
// TaskStatusEvent 是一次任务状态流转事件(经 SubjectTaskStatus 回流给网关)。
type TaskStatusEvent struct {
TaskID string `json:"task_id"`
Status string `json:"status"` // running / done / failed / timeout / waiting / rejected
Detail string `json:"detail,omitempty"` // 失败原因 / 审批摘要等
TS int64 `json:"ts"` // unix 毫秒
}
// ApprovalSubject 返回某任务的人工审批决定回传主题。
func ApprovalSubject(id string) string { return SubjectApproval + "." + id }
// ApprovalDecision 是一次人工审批的结果(UI → 网关 → dispatcher)。
type ApprovalDecision struct {
TaskID string `json:"task_id"`
Node string `json:"node,omitempty"` // 审批节点 id(可空:按 task 维度兜底匹配)
Approved bool `json:"approved"` // true=批准放行,false=拒绝中止
Note string `json:"note,omitempty"` // 审批人备注
By string `json:"by,omitempty"` // 审批人(用户 id
TS int64 `json:"ts"` // unix 毫秒
}
const (
// MetaUserID 是 Task.Meta 中承载已登录用户标识的键(用于偏好记忆召回)。
MetaUserID = "user_id"
// MetaTenantID 是 Task.Meta 中承载租户标识的键(用于用量按租户计量 / 计费)。
MetaTenantID = "tenant_id"
// MetaSessionID 是 Task.Meta 中承载会话标识的键(用于短期多轮历史)。
MetaSessionID = "session_id"
// MetaSafetyCheck 是输入护栏「灰区升级」标志:网关 Tier1(归一化+正则)判为疑似但不确定时置 true,
// Dispatcher 执行前据此调 LLM jailbreak 分类器(Tier2)裁决。明确干净/明确恶意的输入不带此标志,不付 LLM 成本。
MetaSafetyCheck = "safety_check"
// MetaTokenBudget 是单任务 token 预算上限(数字):网关按用户/套餐下发,Dispatcher 据此封顶单任务用量,
// 触顶即中止(防失控成本)。缺省用 Dispatcher 的 env TASK_TOKEN_BUDGET。
MetaTokenBudget = "token_budget"
// 配置控制面按 kind 寻址:sundynix.config.<kind>.get / .updated。
// Gateway 持有配置,消费方(Dispatcher/mcp-go)经 NATS 取用/订阅变更。
ConfigKindChat = "chat" // 工作主力对话模型(Dispatcher 默认用,要强)
ConfigKindEmbedding = "embedding" // 向量模型(mcp-go RAG 用)
ConfigKindVoice = "voice" // JARVIS 语音对话模型(Dispatcher 给语音任务用,要快/低时延)
// MetaModelProfile 指定该任务用哪档模型:为 ModelProfileVoice 时 Dispatcher 走语音模型池
// (未配置语音模型则透明回落工作模型)。语音任务由网关置此标记,与 intent==report 同类路由。
MetaModelProfile = "model_profile"
ModelProfileVoice = "voice"
// 报告生成:Task.Meta[MetaIntent]==IntentReport 时,Dispatcher 走专用多步编排
// (规划大纲 → 各章节并行检索+撰写 → 汇聚 → 渲染 Word),而非通用对话图。
MetaIntent = "intent"
IntentReport = "report"
MetaTopic = "topic" // 报告主题
MetaKB = "kb" // 报告依据的知识库(可空,则不挂检索)
)
// ConfigGetSubject / ConfigUpdatedSubject 返回某类配置的 request / 广播主题。
func ConfigGetSubject(kind string) string { return "sundynix.config." + kind + ".get" }
func ConfigUpdatedSubject(kind string) string { return "sundynix.config." + kind + ".updated" }
// SubjectExec 是执行可视化事件的回流前缀;实际 sundynix.exec.<task_id>。
// 与 Token 流(sundynix.streams.<id>)分流:Token 是零拷贝字节,Exec 是结构化节点事件。
const SubjectExec = "sundynix.exec"
// ExecSubject 返回某任务的执行事件回流主题。
func ExecSubject(id string) string { return SubjectExec + "." + id }
// ExecEvent 是一次任务执行中某节点/阶段的生命周期事件(经 sundynix.exec.<id> 回流给 UI
// 用于"运行·观测"的实时轨迹:节点点亮、工具调用入参/产出、各阶段耗时)。
type ExecEvent struct {
Seq int `json:"seq"` // 任务内自增序号(保序)
TS int64 `json:"ts"` // unix 毫秒
Node string `json:"node"` // 稳定节点 idinit / tool:wiki_search / prompt / model / plan / section:0 / render
Kind string `json:"kind"` // 归类着色:memory|tool|prompt|model|plan|section|render|system
Phase string `json:"phase"` // start|end|error|info
Label string `json:"label"` // 人读标题
Detail string `json:"detail,omitempty"` // 入参/产出/计数预览
MS int64 `json:"ms,omitempty"` // end 事件的耗时(毫秒)
}
// TripleView 是回流给 UI 的一条知识三元组(主体-关系-客体),用于实时展示抽取过程与图谱。
type TripleView struct {
S string `json:"s"`
P string `json:"p"`
O string `json:"o"`
}
// IngestEvent 是入库流水线的实时进度事件(经 sundynix.streams.<job_id> 回流给 UI)。
type IngestEvent struct {
Stage string `json:"stage"` // 解析/解析完成/切块/向量化/写Milvus/写Bleve/抽实体/写Neo4j/完成/失败
Msg string `json:"msg,omitempty"` // 文案
Done int `json:"done,omitempty"` // 进度(如已向量化块数)
Total int `json:"total,omitempty"` // 总数
Chunks []string `json:"chunks,omitempty"` // 切块预览(切块阶段发一次)
Preview string `json:"preview,omitempty"` // 解析阶段:解析出的文本片段预览
Triples []TripleView `json:"triples,omitempty"` // 抽实体阶段:LLM 抽出的知识三元组(实时浮现 + 喂图谱)
Error string `json:"error,omitempty"`
}
// ModelConfig 是一个模型后端的连接配置(provider 抽象,chat 与 embedding 同形)。
// 开发期指向第三方在线 API(OpenAI 兼容);生产期可换自部署或其它在线模型。
type ModelConfig struct {
Provider string `json:"provider"` // openai-compatible / vllm / ...
BaseURL string `json:"base_url"` // 如 https://api.deepseek.com
APIKey string `json:"api_key,omitempty"`
Model string `json:"model"` // 如 deepseek-chat / text-embedding-v3
// Fallbacks 是主模型调用失败/超时时按序切换的备用模型(仅 chat 用,骑在主配置里一并下发)。
// 网关把"其它 enabled chat 模型"填进来;dispatcher 据此把模型包成 failover 链。
Fallbacks []ModelConfig `json:"fallbacks,omitempty"`
}
// Ready 报告该配置是否足以发起真实推理。
func (m *ModelConfig) Ready() bool {
return m != nil && m.BaseURL != "" && m.Model != ""
}
// Task 是 DSL 解析组装后的可调度任务,在 NATS 上以 JSON 传输。
type Task struct {
ID string `json:"id"`
Graph json.RawMessage `json:"graph"` // React Flow 导出的 Agent 编排图
Meta map[string]any `json:"meta,omitempty"`
}
// TaskSubject 返回某任务的发布主题。
func TaskSubject(id string) string { return SubjectTasks + "." + id }
// StreamSubject 返回某任务的 Token 回流主题。
func StreamSubject(id string) string { return SubjectStream + "." + id }
// ToolSubjectGo / ToolSubjectPy 返回某工具的调用主题。
func ToolSubjectGo(tool string) string { return SubjectToolsGo + "." + tool }
func ToolSubjectPy(tool string) string { return SubjectToolsPy + "." + tool }
// ToolSubjectPlatform 平台工具主题(gateway 提供,JARVIS 中枢用)。
func ToolSubjectPlatform(tool string) string { return SubjectToolsPlatform + "." + tool }
// VoiceEventSubject 某用户的语音事件主题(JARVIS 动作/主动播报)。
func VoiceEventSubject(uid string) string { return SubjectVoiceEvent + "." + uid }
// LocalExecSubject 某用户的本地执行路由主题(local_* 工具 → 桌面 runner)。
func LocalExecSubject(uid string) string { return SubjectLocalExec + "." + uid }
// VoiceEvent 是发往用户语音会话的一条事件:
// - action=navigate:让客户端界面跳转(view + 可选 task_id
// - action=announce:主动播报(text 会被 TTS 念出来 + 对话流显示)
type VoiceEvent struct {
Action string `json:"action"`
View string `json:"view,omitempty"`
TaskID string `json:"task_id,omitempty"`
Text string `json:"text,omitempty"`
}
// ToolCall 是 Dispatcher 对一个 MCP 工具的调用请求(NATS request 体)。
type ToolCall struct {
Tool string `json:"tool"` // 工具名,如 wiki_search
Args map[string]any `json:"args,omitempty"` // 工具参数
TaskID string `json:"task_id,omitempty"` // 触发该调用的任务(便于追踪)
}
// ToolResult 是 MCP 工具的应答(NATS reply 体)。
type ToolResult struct {
OK bool `json:"ok"`
Content string `json:"content,omitempty"` // 工具产出(如检索结果文本)
Error string `json:"error,omitempty"` // 非空表示工具内部出错
}
// Marshal / Unmarshal 便捷方法。
func (t *Task) Marshal() ([]byte, error) { return json.Marshal(t) }
func Unmarshal(b []byte) (*Task, error) {
var t Task
if err := json.Unmarshal(b, &t); err != nil {
return nil, err
}
return &t, nil
}