diff --git a/README.md b/README.md index 2d2c000..9673c25 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,25 @@ # Agent-System-Core -系统核心契约仓:领域模型、流式事件协议、HITL 卡片协议、插件 API、设置与 UI 扩展词汇、文档解析/检索/记忆契约。Go 模块路径 agent-system-core;同时发布 JSON Schema 与 TS 类型。不含实现。 +系统核心契约:领域模型(Project/Conversation/Message/Run/Artifact/Plan)、流式事件信封、 +HITL 卡片协议、插件 API 与 manifest、AgentProfile 画像、设置 schema、UI 扩展词汇、 +文档解析(ParsedDocument IR)与检索/记忆契约。Go 模块路径 `agent-system-core`。 -契约与架构见 Agent-System-Documents 设计方案与架构文档;开发规范见 development-guidelines(Dev 仓 `development/guidelines`)。本仓为 Agent-System 多仓组合的组成部分。 +## 三态一致 + +同一契约以三种形态发布,变更须保持一致: + +1. **Go 类型**(`*.go`,camelCase JSON 标签)——后端与代码插件使用。 +2. **黄金样本**(`testdata/golden/*.json`)——线格式权威,`go test ./...` 强校验;变更显式重生成(`go test ./... -update`)。 +3. **TS 类型**(`ts/types.ts`)——前端镜像,前端测试按黄金样本做形状断言。 + +## 原则 + +- 只含契约与纯函数,不含实现、不含 I/O、不依赖生态仓。 +- 前向兼容:字段只增不删;消费方对未知 `type`/词汇静默忽略(`v` 字段协商)。 +- 摄取 IR(ParsedDocument)是解析器与下游的隔离层;检索/记忆契约先行、实现后置。 + +## 验证 + +```bash +go test ./... +``` diff --git a/conformance_test.go b/conformance_test.go new file mode 100644 index 0000000..e3ef06c --- /dev/null +++ b/conformance_test.go @@ -0,0 +1,179 @@ +package core + +import ( + "encoding/json" + "flag" + "os" + "path/filepath" + "testing" +) + +// 黄金样本一致性:Go 序列化(线格式契约)与 testdata/golden/*.json 逐字节一致。 +// ts/types.ts 镜像同一契约;前端测试按样本做形状断言。变更契约须显式更新样本。 +var update = flag.Bool("update", false, "rewrite golden files") + +func golden(t *testing.T, name string, v any) { + t.Helper() + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + t.Fatal(err) + } + path := filepath.Join("testdata", "golden", name+".json") + if *update { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(path) + if err != nil { + t.Fatalf("golden missing (run go test ./... -update): %v", err) + } + if string(want) != string(append(data, '\n')) { + t.Errorf("%s: golden mismatch:\n got: %s\nwant: %s", name, data, want) + } +} + +func sampleMessage() Message { + return Message{ + ID: "msg_abc123def456", ConversationID: "cnv_abc123def456", RunID: "run_abc123def456", + Role: RoleAssistant, + Parts: []Part{ + {Type: PartThinking, Text: "考虑先检索…", Process: true}, + {Type: PartToolCall, Process: true, ToolCall: &ToolCallPart{ID: "call_1", Name: "history_search", Arguments: `{"q":"部署"}`, Duration: 12.5}}, + {Type: PartToolResult, Process: true, ToolResult: &ToolResultPart{CallID: "call_1", Status: "ok", Summary: "3 条命中"}}, + {Type: PartText, Text: "根据历史记录……"}, + {Type: PartArtifactRef, Artifact: &ArtifactRef{ArtifactID: "art_abc123def456", Name: "报告.md", Kind: "document"}}, + }, + } +} + +func sampleRun() Run { + return Run{ + ID: "run_abc123def456", ConversationID: "cnv_abc123def456", ProjectID: "prj_abc123def456", + Kind: RunConversation, ProfileID: "default", Status: RunCompleted, Phase: PhaseDone, + Usage: &Usage{PromptTokens: 1200, CompletionTokens: 300, TotalTokens: 1500, DurationMs: 4200.5, ToolCalls: 2}, + } +} + +func TestGoldenDomain(t *testing.T) { + golden(t, "message", sampleMessage()) + golden(t, "run", sampleRun()) + golden(t, "project", Project{ID: "prj_abc123def456", Name: "示例项目"}) + golden(t, "conversation", Conversation{ID: "cnv_abc123def456", ProjectID: "prj_abc123def456", Title: "新会话"}) + golden(t, "artifact", Artifact{ID: "art_abc123def456", ProjectID: "prj_abc123def456", Name: "报告.md", Kind: "document", MIME: "text/markdown", Size: 128}) +} + +func TestGoldenEvents(t *testing.T) { + golden(t, "event_message_delta", StreamEvent{Seq: 42, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:00Z", V: 1, Type: EvMessageDelta, Payload: json.RawMessage(`{"index":0,"text":"你"}`)}) + golden(t, "event_agent_spawn", StreamEvent{Seq: 43, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:01Z", V: 1, Type: EvAgentSpawn, Payload: json.RawMessage(`{"agentRun":"run_sub1","name":"检索员","task":"查法规"}`)}) + golden(t, "event_run_completed", StreamEvent{Seq: 44, RunID: "run_1", ConversationID: "cnv_1", TS: "2026-09-29T10:00:02Z", V: 1, Type: EvRunCompleted, Payload: json.RawMessage(`{"usage":{"promptTokens":100,"completionTokens":20,"totalTokens":120,"durationMs":1500,"toolCalls":1}}`)}) +} + +func TestGoldenHITL(t *testing.T) { + golden(t, "hitl_card", HITLCard{ + ID: "hitl_abc123def456", RunID: "run_1", ConversationID: "cnv_1", + Question: "采用哪种输出格式?", Kind: HITLKindChoice, + Choices: []HITLChoice{ + {ID: "md", Label: "Markdown 文档", Description: "可版本管理的轻量文档", Impact: "写入 artifacts,可在右栏查看", Recommended: true}, + {ID: "docx", Label: "Word 文档", Description: "正式交付格式", Impact: "生成 docx 工件"}, + }, + AllowFreeInput: true, FreeInputHint: "也可以直接输入其他格式要求", + }) +} + +func TestGoldenProfileAndTools(t *testing.T) { + maxT := 40 + golden(t, "profile", AgentProfile{ + ID: "default", Name: "通用助手", Builtin: true, Source: "builtin", + SystemPrompt: "你是一个通用智能体助手。", + Model: ModelRoute{Chat: "gpt-default", Lightweight: "gpt-lite"}, + Tools: ToolPolicy{Allow: []string{"*"}, Deny: []string{"http.admin."}, Confirm: []string{"http."}}, + Context: ContextPolicy{MaxHistoryMessages: 40, MaxToolResultChars: 20000}, + HITL: HITLPolicy{AskOnAmbiguity: true}, + Budget: RunBudget{MaxToolCalls: &maxT}, + }) + golden(t, "tool_http", ToolDesc{ + Name: "http_translate", Description: "调用翻译 API", Source: SourceUser, + Parameters: json.RawMessage(`{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}`), + HTTP: &HTTPToolSpec{Method: "POST", URL: "https://api.example.com/translate", BodyTemplate: `{"q":"{{param.text}}"}`, TimeoutMs: 10000, ResultPath: "data.0"}, + }) +} + +func TestGoldenIngest(t *testing.T) { + golden(t, "parsed_document", ParsedDocument{ + Meta: DocMeta{Filename: "报告.docx", MIME: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", Parser: "builtin:docx", Confidence: 0.92}, + Blocks: []DocBlock{ + {Type: BlockHeading, Level: 1, Text: "概述"}, + {Type: BlockPara, Text: "本文档说明……"}, + {Type: BlockList, Items: []string{"要点一", "要点二"}}, + {Type: BlockTable, Rows: [][]string{{"项", "值"}, {"版本", "1.0"}}}, + }, + }) +} + +func TestGoldenPlugin(t *testing.T) { + golden(t, "plugin_manifest", PluginManifest{ + ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion, + Requires: PluginRequires{Network: []string{"api.example.com"}}, + Provides: PluginProvides{Tools: []string{"http_echo"}, Intents: []string{"demo"}, Panels: []string{"demo"}, Cards: []string{"note"}, Settings: []string{"greeting"}}, + }) +} + +// ---- 纯函数行为 ---- + +func TestToolPolicy(t *testing.T) { + p := ToolPolicy{Allow: []string{"*"}, Deny: []string{"http.admin."}, Confirm: []string{"http."}} + if !p.ToolAllowed("history_search") { + t.Error("allow * should permit builtin tools") + } + if p.ToolAllowed("http.admin.reset") { + t.Error("deny prefix should win over allow *") + } + if !p.ToolAllowed("http.fetch") || !p.NeedsConfirm("http.fetch") { + t.Error("http prefix tools allowed but need confirm") + } + if p.NeedsConfirm("history_search") { + t.Error("builtin tool should not need confirm") + } +} + +func TestPlainText(t *testing.T) { + d := ParsedDocument{Blocks: []DocBlock{ + {Type: BlockHeading, Level: 2, Text: "标题"}, + {Type: BlockPara, Text: "段落"}, + {Type: BlockTable, Rows: [][]string{{"a", "b"}}}, + }} + got := d.PlainText() + for _, want := range []string{"## 标题", "段落", "a\tb"} { + if !contains(got, want) { + t.Errorf("PlainText missing %q in %q", want, got) + } + } +} + +func TestSettingsMergeAndPath(t *testing.T) { + base := map[string]any{"models": map[string]any{"chat": "a", "temperature": 0.7}, "ui": map[string]any{"theme": "light"}} + over := map[string]any{"models": map[string]any{"chat": "b"}, "ui": map[string]any{"theme": "dark"}} + eff := EffectiveSettings(MergeSettings(base, over)) + if eff.GetString("models.chat", "") != "b" || eff.GetNumber("models.temperature", 0) != 0.7 { + t.Error("deep merge failed") + } + if eff.GetString("models.missing", "def") != "def" { + t.Error("missing path should return default") + } +} + +func contains(s, sub string) bool { + return len(s) >= len(sub) && (func() bool { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false + })() +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..2fefe06 --- /dev/null +++ b/data.go @@ -0,0 +1,146 @@ +package core + +import "time" + +// ---- 文档摄取与解析(4.11)---- + +// ParsedDocument 是解析中间表示:所有下游(附件上下文/工件预览/检索索引/全文检索) +// 只消费 IR,不关心解析器——解析器可替换、可组合、可交叉验证。 +type ParsedDocument struct { + Meta DocMeta `json:"meta"` + Blocks []DocBlock `json:"blocks"` + Warnings []string `json:"warnings,omitempty"` +} + +type DocMeta struct { + Filename string `json:"filename"` + MIME string `json:"mime,omitempty"` + Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent + Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低 + CrossCheck *CrossCheck `json:"crossCheck,omitempty"` + ParsedAt time.Time `json:"parsedAt"` +} + +type CrossCheck struct { + With string `json:"with"` // 交叉验证的另一解析器 + DiffBlocks int `json:"diffBlocks"` + Verdict string `json:"verdict"` // agree | differ +} + +type DocBlockType string + +const ( + BlockHeading DocBlockType = "heading" + BlockPara DocBlockType = "para" + BlockList DocBlockType = "list" + BlockTable DocBlockType = "table" + BlockCode DocBlockType = "code" + BlockImageRef DocBlockType = "image-ref" +) + +type DocBlock struct { + Type DocBlockType `json:"type"` + Level int `json:"level,omitempty"` // heading + Text string `json:"text,omitempty"` // para/heading/list 项拼接/code + Items []string `json:"items,omitempty"` // list + Rows [][]string `json:"rows,omitempty"` // table(首行为表头) + Ref string `json:"ref,omitempty"` // image-ref 的对象键 + Confidence float64 `json:"confidence,omitempty"` +} + +// PlainText 把文档压成纯文本(附件注入与全文索引用);表格按行拼接。 +func (d ParsedDocument) PlainText() string { + out := make([]byte, 0, 1024) + for _, b := range d.Blocks { + switch b.Type { + case BlockHeading: + out = append(out, '\n') + for i := 0; i < b.Level; i++ { + out = append(out, '#') + } + out = append(out, ' ') + out = append(out, b.Text...) + out = append(out, '\n') + case BlockPara, BlockCode: + out = append(out, b.Text...) + out = append(out, '\n') + case BlockList: + for _, it := range b.Items { + out = append(out, "- "...) + out = append(out, it...) + out = append(out, '\n') + } + case BlockTable: + for _, row := range b.Rows { + for i, cell := range row { + if i > 0 { + out = append(out, '\t') + } + out = append(out, cell...) + } + out = append(out, '\n') + } + } + out = append(out, '\n') + } + return string(out) +} + +// ParserDesc 注册表条目(来源标记:builtin/plugin/...;一切可禁用)。 +type ParserDesc struct { + ID string `json:"id"` + Formats []string `json:"formats"` // 支持的扩展名 + Source SourceKind `json:"source"` + Note string `json:"note,omitempty"` +} + +// ---- 检索(4.12)---- + +// KnowledgeDoc 是检索语料的一条文档(来自 ingest 或插件知识库,带来源标记)。 +type KnowledgeDoc struct { + ID ID `json:"id"` + ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库 + Source string `json:"source"` // upload | artifact | plugin: + Title string `json:"title"` + Ref string `json:"ref,omitempty"` // 工件/文件引用 + Chunk string `json:"chunk"` + CreatedAt time.Time `json:"createdAt"` +} + +type RetrievalHit struct { + Doc KnowledgeDoc `json:"doc"` + Score float64 `json:"score"` + Kind string `json:"kind"` // fulltext | vector | plugin +} + +type RetrievalQuery struct { + ProjectID ID `json:"projectId,omitempty"` + Text string `json:"text"` + TopK int `json:"topK,omitempty"` + Sources []string `json:"sources,omitempty"` // 过滤来源 +} + +// ---- 记忆(4.13 契约;实现远期)---- + +type MemoryEntry struct { + ID string `json:"id"` + Scope string `json:"scope"` // user | project + ProjectID ID `json:"projectId,omitempty"` + Type string `json:"type"` // fact | preference | decision | episode | procedure + Content string `json:"content"` + Source string `json:"source"` // run: | manual + Confidence float64 `json:"confidence,omitempty"` + CreatedAt time.Time `json:"createdAt"` + LastAccessed time.Time `json:"lastAccessed"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` +} + +// ---- 上传文件 ---- + +type UploadInfo struct { + ArtifactID ID `json:"artifactId"` + Filename string `json:"filename"` + MIME string `json:"mime,omitempty"` + Size int64 `json:"size"` + Parsed *ParsedDocument `json:"parsed,omitempty"` +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..814e759 --- /dev/null +++ b/events.go @@ -0,0 +1,122 @@ +package core + +import "encoding/json" + +// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。 +// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。 +type StreamEvent struct { + Seq int64 `json:"seq"` + RunID ID `json:"runId,omitempty"` + ConversationID ID `json:"conversationId"` + TS string `json:"ts"` // RFC3339 + V int `json:"v"` + Type StreamEventType `json:"type"` + Payload json.RawMessage `json:"payload,omitempty"` +} + +type StreamEventType string + +const ( + EvMessageDelta StreamEventType = "message.delta" + EvThinkingDelta StreamEventType = "thinking.delta" + EvToolCall StreamEventType = "tool.call" + EvToolResult StreamEventType = "tool.result" + EvRunPhase StreamEventType = "run.phase" + EvHITLRequest StreamEventType = "hitl.request" + EvHITLResolved StreamEventType = "hitl.resolved" + EvPlanUpdate StreamEventType = "plan.update" + EvTodoUpdate StreamEventType = "todo.update" + EvAgentSpawn StreamEventType = "agent.spawn" + EvAgentProgress StreamEventType = "agent.progress" + EvAgentResult StreamEventType = "agent.result" + EvArtifactCreated StreamEventType = "artifact.created" + EvArtifactUpdated StreamEventType = "artifact.updated" + EvRunCompleted StreamEventType = "run.completed" + EvRunFailed StreamEventType = "run.failed" + EvError StreamEventType = "error" +) + +// ProtocolVersion 是事件信封与 payload 词汇的当前版本。 +const ProtocolVersion = 1 + +// ---- 各事件 payload(新增字段只增不删;消费方忽略未知字段)---- + +// MessageDeltaPayload:正文增量。Index 标识目标 Part 序号(流式聚合用)。 +type MessageDeltaPayload struct { + Index int `json:"index"` + Text string `json:"text"` +} + +type ThinkingDeltaPayload struct { + Index int `json:"index"` + Text string `json:"text"` +} + +type ToolCallPayload struct { + CallID string `json:"callId"` + Name string `json:"name"` + Arguments string `json:"arguments,omitempty"` +} + +type ToolResultPayload struct { + CallID string `json:"callId"` + Status string `json:"status"` + Summary string `json:"summary,omitempty"` + Truncated bool `json:"truncated,omitempty"` +} + +type RunPhasePayload struct { + Phase RunPhase `json:"phase"` +} + +type HITLResolvedPayload struct { + CardID ID `json:"cardId"` + Answer string `json:"answer,omitempty"` + Kind string `json:"kind"` // choice | free | timeout | cancelled +} + +type PlanUpdatePayload struct { + Plan PlanSnapshot `json:"plan"` +} + +type TodoUpdatePayload struct { + List TodoList `json:"list"` +} + +// AgentSpawnPayload:子代理派生;AgentRun 即子 Run(父子关系即任务树)。 +type AgentSpawnPayload struct { + AgentRun ID `json:"agentRun"` + Name string `json:"name"` + Task string `json:"task"` +} + +type AgentProgressPayload struct { + AgentRun ID `json:"agentRun"` + Note string `json:"note,omitempty"` + Phase RunPhase `json:"phase,omitempty"` +} + +type AgentResultPayload struct { + AgentRun ID `json:"agentRun"` + Status RunStatus `json:"status"` + Summary string `json:"summary,omitempty"` +} + +type ArtifactPayload struct { + Artifact Artifact `json:"artifact"` +} + +type RunCompletedPayload struct { + Usage Usage `json:"usage"` +} + +type RunFailedPayload struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type ErrorPayload struct { + Code string `json:"code"` + Message string `json:"message"` + RunID ID `json:"runId,omitempty"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f495345 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module agent-system-core + +go 1.27 diff --git a/hitl.go b/hitl.go new file mode 100644 index 0000000..d71c007 --- /dev/null +++ b/hitl.go @@ -0,0 +1,45 @@ +package core + +import "encoding/json" + +// HITL 卡片协议(4.5):choice | input | confirm | form 四种; +// 发起方必须给 Choices 提供 description 与 impact——用户在决策点需要上下文。 +type HITLCard struct { + ID ID `json:"id"` + RunID ID `json:"runId"` + ConversationID ID `json:"conversationId"` + Question string `json:"question"` + Kind HITLKind `json:"kind"` + Choices []HITLChoice `json:"choices,omitempty"` + AllowFreeInput bool `json:"allowFreeInput"` + FreeInputHint string `json:"freeInputHint,omitempty"` + TimeoutSec *int `json:"timeoutSec,omitempty"` + Form []UINode `json:"form,omitempty"` // form kind 的字段(UI 词汇) + CreatedAt string `json:"createdAt"` + Resolved bool `json:"resolved"` +} + +type HITLKind string + +const ( + HITLKindChoice HITLKind = "choice" + HITLKindInput HITLKind = "input" + HITLKindConfirm HITLKind = "confirm" + HITLKindForm HITLKind = "form" +) + +type HITLChoice struct { + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Impact string `json:"impact"` // 该选择可能造成的影响(必填) + Recommended bool `json:"recommended,omitempty"` +} + +// HITLResponse 是用户对卡片的响应;自由输入时 ChoiceID 为空。 +type HITLResponse struct { + CardID ID `json:"cardId"` + ChoiceID string `json:"choiceId,omitempty"` + FreeText string `json:"freeText,omitempty"` + FormData json.RawMessage `json:"formData,omitempty"` +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..a161277 --- /dev/null +++ b/model.go @@ -0,0 +1,217 @@ +// Package core 定义 Agent-System 的核心契约:领域模型、流式事件、HITL 卡片、 +// 插件 API、设置与 UI 词汇。本仓只含契约与少量纯函数,不含实现、不含 I/O。 +// +// JSON 标签(camelCase)是前后端与插件共同的线格式契约;TS 类型镜像见 ts/types.ts, +// 黄金样本见 testdata/golden/(conformance 测试保证 Go 序列化与样本一致)。 +package core + +import "time" + +// ---- 标识 ---- + +// ID 形态为 _;生成由后端负责,契约只约束非空与唯一。 +type ID = string + +// ---- 项目与会话 ---- + +type Project struct { + ID ID `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Settings map[string]any `json:"settings,omitempty"` // 项目层设置覆盖(4.9 覆盖链) +} + +type Conversation struct { + ID ID `json:"id"` + ProjectID ID `json:"projectId"` + Title string `json:"title"` + ProfileID ID `json:"profileId,omitempty"` // 空为默认画像 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// ---- 消息与部件 ---- + +type Role string + +const ( + RoleUser Role = "user" + RoleAssistant Role = "assistant" + RoleSystem Role = "system" +) + +// PartType 区分消息成分;前端按类型查渲染器,未知类型静默忽略(原则 6)。 +type PartType string + +const ( + PartText PartType = "text" + PartThinking PartType = "thinking" + PartToolCall PartType = "tool_call" + PartToolResult PartType = "tool_result" + PartArtifactRef PartType = "artifact_ref" + PartHITL PartType = "hitl" + PartPlan PartType = "plan" + PartError PartType = "error" + PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema) +) + +type Message struct { + ID ID `json:"id"` + ConversationID ID `json:"conversationId"` + RunID ID `json:"runId,omitempty"` + Role Role `json:"role"` + Parts []Part `json:"parts"` + CreatedAt time.Time `json:"createdAt"` +} + +// Part 是判别联合:Type 决定哪些字段有意义;未用字段为零值。 +type Part struct { + Type PartType `json:"type"` + + Text string `json:"text,omitempty"` // text / thinking(流式增量聚合后) + + ToolCall *ToolCallPart `json:"toolCall,omitempty"` + ToolResult *ToolResultPart `json:"toolResult,omitempty"` + Artifact *ArtifactRef `json:"artifact,omitempty"` + Hitl *HITLSummary `json:"hitl,omitempty"` + Plan *PlanSnapshot `json:"plan,omitempty"` + Card *CardPart `json:"card,omitempty"` + Err *ErrorPart `json:"error,omitempty"` + + // 过程标记:true 的部件在 run 终态后由前端折叠为摘要行(4.2)。 + Process bool `json:"process,omitempty"` +} + +type ToolCallPart struct { + ID string `json:"id"` + Name string `json:"name"` + Arguments string `json:"arguments,omitempty"` // 原始 JSON + Duration float64 `json:"durationMs,omitempty"` +} + +type ToolResultPart struct { + CallID string `json:"callId"` + Status string `json:"status"` // ok | error + Summary string `json:"summary,omitempty"` + Truncated bool `json:"truncated,omitempty"` // 超长截断并转工件引用(4.10) +} + +type ArtifactRef struct { + ArtifactID ID `json:"artifactId"` + Name string `json:"name"` + Kind string `json:"kind,omitempty"` // document | image | data | ... +} + +type HITLSummary struct { + CardID ID `json:"cardId"` + Question string `json:"question"` + Resolved bool `json:"resolved"` + Answer string `json:"answer,omitempty"` // 选中项 label 或回填文本 +} + +type ErrorPart struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// CardPart.Data 是 UI schema 节点(4.6 有限词汇)。 +type CardPart struct { + Kind string `json:"kind"` + Data []UINode `json:"data"` +} + +// ---- Run(一切运行皆 Run)---- + +type RunKind string + +const ( + RunConversation RunKind = "conversation" + RunSubagent RunKind = "subagent" + RunTool RunKind = "tool" +) + +type RunStatus string + +const ( + RunPending RunStatus = "pending" + RunRunning RunStatus = "running" + RunWaitingHITL RunStatus = "waiting_hitl" + RunCompleted RunStatus = "completed" + RunFailed RunStatus = "failed" + RunCancelled RunStatus = "cancelled" + RunInterrupted RunStatus = "interrupted" +) + +type RunPhase string + +const ( + PhaseThinking RunPhase = "thinking" + PhaseActing RunPhase = "acting" + PhaseResponding RunPhase = "responding" + PhaseDone RunPhase = "done" +) + +type Run struct { + ID ID `json:"id"` + ParentRunID ID `json:"parentRunId,omitempty"` + ConversationID ID `json:"conversationId"` + ProjectID ID `json:"projectId"` + Kind RunKind `json:"kind"` + ProfileID ID `json:"profileId,omitempty"` + Status RunStatus `json:"status"` + Phase RunPhase `json:"phase,omitempty"` + AgentName string `json:"agentName,omitempty"` // subagent 显示名 + CreatedAt time.Time `json:"createdAt"` + EndedAt *time.Time `json:"endedAt,omitempty"` + Usage *Usage `json:"usage,omitempty"` + Error *ErrorPart `json:"error,omitempty"` +} + +type Usage struct { + PromptTokens int `json:"promptTokens"` + CompletionTokens int `json:"completionTokens"` + TotalTokens int `json:"totalTokens"` + DurationMs float64 `json:"durationMs"` + ToolCalls int `json:"toolCalls"` +} + +// ---- 工件 ---- + +type Artifact struct { + ID ID `json:"id"` + ProjectID ID `json:"projectId"` + Name string `json:"name"` + Kind string `json:"kind"` + MIME string `json:"mime,omitempty"` + Size int64 `json:"size"` + SourceRun ID `json:"sourceRun,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// ---- 计划与待办(右栏 Plan / Todos)---- + +type PlanSnapshot struct { + PlanID ID `json:"planId"` + Title string `json:"title"` + Steps []PlanStep `json:"steps"` +} + +type PlanStep struct { + ID string `json:"id"` + Text string `json:"text"` + Status string `json:"status"` // pending | in_progress | done | skipped + AgentRun ID `json:"agentRun,omitempty"` +} + +type TodoList struct { + ConversationID ID `json:"conversationId"` + Items []TodoItem `json:"items"` +} + +type TodoItem struct { + ID string `json:"id"` + Text string `json:"text"` + Status string `json:"status"` // pending | in_progress | done +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..f85edb8 --- /dev/null +++ b/plugin.go @@ -0,0 +1,69 @@ +package core + +import "encoding/json" + +// 插件 manifest(4.6):插件 = manifest + 声明资产 + 可选代码。 +// 校验失败整批拒绝(fail-closed);卸载注销注册项,在途引用按定义缺失稳定报错。 + +const PluginAPIVersion = "1.0" + +type PluginManifest struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + APIVersion string `json:"apiVersion"` + Requires PluginRequires `json:"requires,omitempty"` + Provides PluginProvides `json:"provides"` +} + +type PluginRequires struct { + Network []string `json:"network,omitempty"` // 允许访问的域名(展示用,v1 自装自审) +} + +type PluginProvides struct { + Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名 + Workflows []string `json:"workflows,omitempty"` + Intents []string `json:"intents,omitempty"` + Prompts []string `json:"prompts,omitempty"` + Panels []string `json:"panels,omitempty"` + Cards []string `json:"cards,omitempty"` + Settings []string `json:"settings,omitempty"` +} + +// PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。 +type PluginPanel struct { + ID string `json:"id"` + Title string `json:"title"` + Icon string `json:"icon,omitempty"` + Body []UINode `json:"body"` + // Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。 + RefreshTool string `json:"refreshTool,omitempty"` +} + +// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。 +type WorkflowDef struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动) + Steps []WorkflowStep `json:"steps"` +} + +type WorkflowStep struct { + ID string `json:"id"` + Title string `json:"title"` + Kind string `json:"kind"` // prompt | tool | hitl | artifact + Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板 + Tool string `json:"tool,omitempty"` // tool: 工具名 + Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板 + Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板 + Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板 +} + +type HITLCardTemplate struct { + Question string `json:"question"` + Kind HITLKind `json:"kind"` + Choices []HITLChoice `json:"choices,omitempty"` + AllowFreeInput bool `json:"allowFreeInput"` + FreeInputHint string `json:"freeInputHint,omitempty"` +} diff --git a/profile.go b/profile.go new file mode 100644 index 0000000..e08e97c --- /dev/null +++ b/profile.go @@ -0,0 +1,205 @@ +package core + +import "encoding/json" + +// 智能体画像(4.7):核心定制机制,全部数据化、可导入导出。 +type AgentProfile struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Builtin bool `json:"builtin,omitempty"` + Source string `json:"source,omitempty"` // builtin | plugin | user + SystemPrompt string `json:"systemPrompt"` + Model ModelRoute `json:"model"` + Tools ToolPolicy `json:"tools"` + Context ContextPolicy `json:"context"` + HITL HITLPolicy `json:"hitl"` + Budget RunBudget `json:"budget"` +} + +// ModelRoute 按用途选择模型(4.7 kind 路由):空串回落到 chat 默认。 +type ModelRoute struct { + Chat string `json:"chat,omitempty"` + Lightweight string `json:"lightweight,omitempty"` + Embedding string `json:"embedding,omitempty"` + Rerank string `json:"rerank,omitempty"` +} + +// ToolPolicy 三清单:allow/deny 决定可见工具,confirm 强制执行前 HITL 确认。 +// 匹配语义:精确名或前缀通配("http.*" 表示 http 前缀);deny 优先于 allow。 +type ToolPolicy struct { + Allow []string `json:"allow"` // ["*"] 全部 + Deny []string `json:"deny,omitempty"` + Confirm []string `json:"confirm,omitempty"` +} + +// ContextPolicy(4.10):ctxasm 的输入;逐版本增加策略项不动组装器接口。 +type ContextPolicy struct { + MaxHistoryMessages int `json:"maxHistoryMessages"` // 0 = 默认 40 + SummarizeBeyond bool `json:"summarizeBeyond,omitempty"` // 远期;先存标志 + MemoryBudgetPct int `json:"memoryBudgetPct,omitempty"` // 注入槽份额上限 + KnowledgeBudgetPct int `json:"knowledgeBudgetPct,omitempty"` + MaxToolResultChars int `json:"maxToolResultChars,omitempty"` // 0 = 默认 20000 +} + +type HITLPolicy struct { + AskOnAmbiguity bool `json:"askOnAmbiguity"` + TimeoutSec *int `json:"timeoutSec,omitempty"` +} + +type RunBudget struct { + MaxTokens *int `json:"maxTokens,omitempty"` + MaxToolCalls *int `json:"maxToolCalls,omitempty"` + MaxTurns *int `json:"maxTurns,omitempty"` // LLM 循环轮上限 +} + +// ToolAllows 判定:deny 优先;allow 含 "*" 或精确/前缀命中。 +func (p ToolPolicy) ToolAllowed(name string) bool { + if matchAny(p.Deny, name) { + return false + } + if len(p.Allow) == 0 { + return true + } + return matchAny(p.Allow, name) +} + +func (p ToolPolicy) NeedsConfirm(name string) bool { + return matchAny(p.Confirm, name) +} + +// matchAny 语义:pattern 精确等于 name,或以 "xxx." 开头表示前缀族(http. → http.*)。 +func matchAny(patterns []string, name string) bool { + for _, p := range patterns { + if p == "*" || p == name { + return true + } + if n := len(p); n > 1 && p[n-1] == '.' { + if len(name) > n-1 && name[:n-1] == p[:n-1] { + return true + } + } + } + return false +} + +// ---- 设置(4.9)---- + +type SettingType string + +const ( + SettingString SettingType = "string" + SettingNumber SettingType = "number" + SettingBool SettingType = "boolean" + SettingEnum SettingType = "enum" + SettingSecret SettingType = "secret" +) + +type SettingEffect string + +const ( + EffectHot SettingEffect = "hot" // 立即生效 + EffectNewRun SettingEffect = "new-run" // 仅影响新 run + EffectRestart SettingEffect = "restart" // 需重启 +) + +// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。 +type SettingDesc struct { + Key string `json:"key"` + Group string `json:"group"` // 分区:models | profiles | tools | knowledge | ui | plugin: + Label string `json:"label"` + Description string `json:"description,omitempty"` + Type SettingType `json:"type"` + Default any `json:"default,omitempty"` + Options []string `json:"options,omitempty"` // enum + Effect SettingEffect `json:"effect"` + Secret bool `json:"secret,omitempty"` +} + +// EffectiveSettings 是覆盖链(system → user → project → conversation)深合并结果。 +type EffectiveSettings map[string]any + +// MergeSettings 深合并:后者覆盖前者;map 递归合并,其它类型直接覆盖(4.9 数组显式覆盖)。 +func MergeSettings(base, override map[string]any) map[string]any { + out := make(map[string]any, len(base)+len(override)) + for k, v := range base { + out[k] = v + } + for k, v := range override { + if bm, ok := base[k].(map[string]any); ok { + if vm, ok2 := v.(map[string]any); ok2 { + out[k] = MergeSettings(bm, vm) + continue + } + } + out[k] = v + } + return out +} + +// GetString/GetBool/GetNumber 读取点路径("models.chat");缺省返回 def。 +func (s EffectiveSettings) GetString(path, def string) string { + v, ok := lookupPath(s, path) + if !ok { + return def + } + if str, ok2 := v.(string); ok2 { + return str + } + return def +} + +func (s EffectiveSettings) GetBool(path string, def bool) bool { + v, ok := lookupPath(s, path) + if !ok { + return def + } + if b, ok2 := v.(bool); ok2 { + return b + } + return def +} + +func (s EffectiveSettings) GetNumber(path string, def float64) float64 { + v, ok := lookupPath(s, path) + if !ok { + return def + } + if f, ok2 := v.(float64); ok2 { + return f + } + return def +} + +func lookupPath(m map[string]any, path string) (any, bool) { + parts := splitPath(path) + var cur any = m + for _, p := range parts { + cm, ok := cur.(map[string]any) + if !ok { + return nil, false + } + cur, ok = cm[p] + if !ok { + return nil, false + } + } + return cur, true +} + +func splitPath(path string) []string { + var out []string + cur := "" + for i := 0; i < len(path); i++ { + switch path[i] { + case '.': + out = append(out, cur) + cur = "" + default: + cur += string(path[i]) + } + } + return append(out, cur) +} + +var _ = json.Marshal // 保持 encoding/json 引用(RawMessage 一致性) diff --git a/testdata/golden/artifact.json b/testdata/golden/artifact.json new file mode 100644 index 0000000..31a0db1 --- /dev/null +++ b/testdata/golden/artifact.json @@ -0,0 +1,10 @@ +{ + "id": "art_abc123def456", + "projectId": "prj_abc123def456", + "name": "报告.md", + "kind": "document", + "mime": "text/markdown", + "size": 128, + "createdAt": "0001-01-01T00:00:00Z", + "updatedAt": "0001-01-01T00:00:00Z" +} diff --git a/testdata/golden/conversation.json b/testdata/golden/conversation.json new file mode 100644 index 0000000..5a2c2b8 --- /dev/null +++ b/testdata/golden/conversation.json @@ -0,0 +1,7 @@ +{ + "id": "cnv_abc123def456", + "projectId": "prj_abc123def456", + "title": "新会话", + "createdAt": "0001-01-01T00:00:00Z", + "updatedAt": "0001-01-01T00:00:00Z" +} diff --git a/testdata/golden/event_agent_spawn.json b/testdata/golden/event_agent_spawn.json new file mode 100644 index 0000000..dded461 --- /dev/null +++ b/testdata/golden/event_agent_spawn.json @@ -0,0 +1,13 @@ +{ + "seq": 43, + "runId": "run_1", + "conversationId": "cnv_1", + "ts": "2026-09-29T10:00:01Z", + "v": 1, + "type": "agent.spawn", + "payload": { + "agentRun": "run_sub1", + "name": "检索员", + "task": "查法规" + } +} diff --git a/testdata/golden/event_message_delta.json b/testdata/golden/event_message_delta.json new file mode 100644 index 0000000..edf4392 --- /dev/null +++ b/testdata/golden/event_message_delta.json @@ -0,0 +1,12 @@ +{ + "seq": 42, + "runId": "run_1", + "conversationId": "cnv_1", + "ts": "2026-09-29T10:00:00Z", + "v": 1, + "type": "message.delta", + "payload": { + "index": 0, + "text": "你" + } +} diff --git a/testdata/golden/event_run_completed.json b/testdata/golden/event_run_completed.json new file mode 100644 index 0000000..972062a --- /dev/null +++ b/testdata/golden/event_run_completed.json @@ -0,0 +1,17 @@ +{ + "seq": 44, + "runId": "run_1", + "conversationId": "cnv_1", + "ts": "2026-09-29T10:00:02Z", + "v": 1, + "type": "run.completed", + "payload": { + "usage": { + "promptTokens": 100, + "completionTokens": 20, + "totalTokens": 120, + "durationMs": 1500, + "toolCalls": 1 + } + } +} diff --git a/testdata/golden/hitl_card.json b/testdata/golden/hitl_card.json new file mode 100644 index 0000000..345e7b0 --- /dev/null +++ b/testdata/golden/hitl_card.json @@ -0,0 +1,26 @@ +{ + "id": "hitl_abc123def456", + "runId": "run_1", + "conversationId": "cnv_1", + "question": "采用哪种输出格式?", + "kind": "choice", + "choices": [ + { + "id": "md", + "label": "Markdown 文档", + "description": "可版本管理的轻量文档", + "impact": "写入 artifacts,可在右栏查看", + "recommended": true + }, + { + "id": "docx", + "label": "Word 文档", + "description": "正式交付格式", + "impact": "生成 docx 工件" + } + ], + "allowFreeInput": true, + "freeInputHint": "也可以直接输入其他格式要求", + "createdAt": "", + "resolved": false +} diff --git a/testdata/golden/message.json b/testdata/golden/message.json new file mode 100644 index 0000000..13b9708 --- /dev/null +++ b/testdata/golden/message.json @@ -0,0 +1,45 @@ +{ + "id": "msg_abc123def456", + "conversationId": "cnv_abc123def456", + "runId": "run_abc123def456", + "role": "assistant", + "parts": [ + { + "type": "thinking", + "text": "考虑先检索…", + "process": true + }, + { + "type": "tool_call", + "toolCall": { + "id": "call_1", + "name": "history_search", + "arguments": "{\"q\":\"部署\"}", + "durationMs": 12.5 + }, + "process": true + }, + { + "type": "tool_result", + "toolResult": { + "callId": "call_1", + "status": "ok", + "summary": "3 条命中" + }, + "process": true + }, + { + "type": "text", + "text": "根据历史记录……" + }, + { + "type": "artifact_ref", + "artifact": { + "artifactId": "art_abc123def456", + "name": "报告.md", + "kind": "document" + } + } + ], + "createdAt": "0001-01-01T00:00:00Z" +} diff --git a/testdata/golden/parsed_document.json b/testdata/golden/parsed_document.json new file mode 100644 index 0000000..01622ad --- /dev/null +++ b/testdata/golden/parsed_document.json @@ -0,0 +1,40 @@ +{ + "meta": { + "filename": "报告.docx", + "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "parser": "builtin:docx", + "confidence": 0.92, + "parsedAt": "0001-01-01T00:00:00Z" + }, + "blocks": [ + { + "type": "heading", + "level": 1, + "text": "概述" + }, + { + "type": "para", + "text": "本文档说明……" + }, + { + "type": "list", + "items": [ + "要点一", + "要点二" + ] + }, + { + "type": "table", + "rows": [ + [ + "项", + "值" + ], + [ + "版本", + "1.0" + ] + ] + } + ] +} diff --git a/testdata/golden/plugin_manifest.json b/testdata/golden/plugin_manifest.json new file mode 100644 index 0000000..e3b2ada --- /dev/null +++ b/testdata/golden/plugin_manifest.json @@ -0,0 +1,28 @@ +{ + "id": "example", + "name": "示例插件", + "version": "0.1.0", + "apiVersion": "1.0", + "requires": { + "network": [ + "api.example.com" + ] + }, + "provides": { + "tools": [ + "http_echo" + ], + "intents": [ + "demo" + ], + "panels": [ + "demo" + ], + "cards": [ + "note" + ], + "settings": [ + "greeting" + ] + } +} diff --git a/testdata/golden/profile.json b/testdata/golden/profile.json new file mode 100644 index 0000000..baac694 --- /dev/null +++ b/testdata/golden/profile.json @@ -0,0 +1,32 @@ +{ + "id": "default", + "name": "通用助手", + "builtin": true, + "source": "builtin", + "systemPrompt": "你是一个通用智能体助手。", + "model": { + "chat": "gpt-default", + "lightweight": "gpt-lite" + }, + "tools": { + "allow": [ + "*" + ], + "deny": [ + "http.admin." + ], + "confirm": [ + "http." + ] + }, + "context": { + "maxHistoryMessages": 40, + "maxToolResultChars": 20000 + }, + "hitl": { + "askOnAmbiguity": true + }, + "budget": { + "maxToolCalls": 40 + } +} diff --git a/testdata/golden/project.json b/testdata/golden/project.json new file mode 100644 index 0000000..78530f7 --- /dev/null +++ b/testdata/golden/project.json @@ -0,0 +1,6 @@ +{ + "id": "prj_abc123def456", + "name": "示例项目", + "createdAt": "0001-01-01T00:00:00Z", + "updatedAt": "0001-01-01T00:00:00Z" +} diff --git a/testdata/golden/run.json b/testdata/golden/run.json new file mode 100644 index 0000000..84f6bb3 --- /dev/null +++ b/testdata/golden/run.json @@ -0,0 +1,17 @@ +{ + "id": "run_abc123def456", + "conversationId": "cnv_abc123def456", + "projectId": "prj_abc123def456", + "kind": "conversation", + "profileId": "default", + "status": "completed", + "phase": "done", + "createdAt": "0001-01-01T00:00:00Z", + "usage": { + "promptTokens": 1200, + "completionTokens": 300, + "totalTokens": 1500, + "durationMs": 4200.5, + "toolCalls": 2 + } +} diff --git a/testdata/golden/tool_http.json b/testdata/golden/tool_http.json new file mode 100644 index 0000000..175476e --- /dev/null +++ b/testdata/golden/tool_http.json @@ -0,0 +1,23 @@ +{ + "name": "http_translate", + "description": "调用翻译 API", + "parameters": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": [ + "text" + ] + }, + "source": "user", + "http": { + "method": "POST", + "url": "https://api.example.com/translate", + "bodyTemplate": "{\"q\":\"{{param.text}}\"}", + "timeoutMs": 10000, + "resultPath": "data.0" + } +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..e4da100 --- /dev/null +++ b/tools.go @@ -0,0 +1,49 @@ +package core + +import "encoding/json" + +// 工具契约(4.8):内置、声明式 HTTP、插件工具统一走 ToolProvider 形态。 +// Parameters 是 JSON Schema(draft-07 子集即可),调用参数按其校验。 + +type SourceKind string + +const ( + SourceBuiltin SourceKind = "builtin" + SourcePlugin SourceKind = "plugin" + SourceUser SourceKind = "user" +) + +type ToolDesc struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters json.RawMessage `json:"parameters,omitempty"` + Source SourceKind `json:"source"` + PluginID string `json:"pluginId,omitempty"` + HTTP *HTTPToolSpec `json:"http,omitempty"` // 声明式 HTTP 工具(无代码) +} + +// HTTPToolSpec:无代码自定义工具(4.8)。模板变量 {{param.x}} 从调用参数注入。 +type HTTPToolSpec struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers,omitempty"` + BodyTemplate string `json:"bodyTemplate,omitempty"` // JSON 模板 + AuthRef string `json:"authRef,omitempty"` // 设置中的密钥引用(settings key) + TimeoutMs int `json:"timeoutMs,omitempty"` + // ResultPath 从响应 JSON 中裁剪(jq 点路径);空取整个响应。 + ResultPath string `json:"resultPath,omitempty"` +} + +type ToolCallRequest struct { + CallID string `json:"callId"` + Name string `json:"name"` + Arguments json.RawMessage `json:"arguments,omitempty"` +} + +type ToolCallResult struct { + CallID string `json:"callId"` + Status string `json:"status"` // ok | error + Output string `json:"output,omitempty"` + Truncated bool `json:"truncated,omitempty"` + Artifact *Artifact `json:"artifact,omitempty"` // 超长转工件 +} diff --git a/ts/types.ts b/ts/types.ts new file mode 100644 index 0000000..cf49f1e --- /dev/null +++ b/ts/types.ts @@ -0,0 +1,198 @@ +// Agent-System-Core 契约的 TypeScript 镜像。 +// 线格式契约以 Go 类型(camelCase JSON 标签)与 testdata/golden/*.json 黄金样本为准; +// 本文件手工镜像同一契约,前端按未知 type/字段静默忽略(前向兼容原则)。 + +export type ID = string; + +// ---- 领域模型 ---- + +export interface Project { + id: ID; name: string; createdAt: string; updatedAt: string; + settings?: Record; +} + +export interface Conversation { + id: ID; projectId: ID; title: string; profileId?: ID; + createdAt: string; updatedAt: string; +} + +export type Role = 'user' | 'assistant' | 'system'; + +export type PartType = + | 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref' + | 'hitl' | 'plan' | 'error' | 'card'; + +export interface Part { + type: PartType; + text?: string; + toolCall?: { id: string; name: string; arguments?: string; durationMs?: number }; + toolResult?: { callId: string; status: 'ok' | 'error'; summary?: string; truncated?: boolean }; + artifact?: { artifactId: ID; name: string; kind?: string }; + hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string }; + plan?: PlanSnapshot; + card?: { kind: string; data: UINode[] }; + error?: { code: string; message: string }; + process?: boolean; // run 终态后折叠为摘要行 +} + +export interface Message { + id: ID; conversationId: ID; runId?: ID; role: Role; + parts: Part[]; createdAt: string; +} + +export type RunKind = 'conversation' | 'subagent' | 'tool'; +export type RunStatus = + | 'pending' | 'running' | 'waiting_hitl' | 'completed' + | 'failed' | 'cancelled' | 'interrupted'; +export type RunPhase = 'thinking' | 'acting' | 'responding' | 'done'; + +export interface Usage { + promptTokens: number; completionTokens: number; totalTokens: number; + durationMs: number; toolCalls: number; +} + +export interface Run { + id: ID; parentRunId?: ID; conversationId: ID; projectId: ID; + kind: RunKind; profileId?: ID; status: RunStatus; phase?: RunPhase; + agentName?: string; createdAt: string; endedAt?: string; + usage?: Usage; error?: { code: string; message: string }; +} + +export interface Artifact { + id: ID; projectId: ID; name: string; kind: string; mime?: string; + size: number; sourceRun?: ID; createdAt: string; updatedAt: string; +} + +export interface PlanSnapshot { + planId: ID; title: string; + steps: { id: string; text: string; status: 'pending' | 'in_progress' | 'done' | 'skipped'; agentRun?: ID }[]; +} + +export interface TodoList { + conversationId: ID; + items: { id: string; text: string; status: 'pending' | 'in_progress' | 'done' }[]; +} + +// ---- 流式事件 ---- + +export type StreamEventType = + | 'message.delta' | 'thinking.delta' | 'tool.call' | 'tool.result' + | 'run.phase' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update' + | 'agent.spawn' | 'agent.progress' | 'agent.result' + | 'artifact.created' | 'artifact.updated' + | 'run.completed' | 'run.failed' | 'error'; + +export interface StreamEvent

{ + seq: number; runId?: ID; conversationId: ID; ts: string; v: number; + type: StreamEventType; payload?: P; +} + +// payload 形状(按 type 判别;未知字段忽略) +export interface MessageDeltaPayload { index: number; text: string } +export interface AgentSpawnPayload { agentRun: ID; name: string; task: string } +export interface AgentProgressPayload { agentRun: ID; note?: string; phase?: RunPhase } +export interface AgentResultPayload { agentRun: ID; status: RunStatus; summary?: string } +export interface HITLResolvedPayload { cardId: ID; answer?: string; kind: 'choice' | 'free' | 'timeout' | 'cancelled' } +export interface RunCompletedPayload { usage: Usage } +export interface RunFailedPayload { code: string; message: string } + +// ---- HITL ---- + +export type HITLKind = 'choice' | 'input' | 'confirm' | 'form'; + +export interface HITLChoice { + id: string; label: string; description: string; impact: string; recommended?: boolean; +} + +export interface HITLCard { + id: ID; runId: ID; conversationId: ID; question: string; kind: HITLKind; + choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string; + timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean; +} + +export interface HITLResponse { + cardId: ID; choiceId?: string; freeText?: string; formData?: unknown; +} + +// ---- UI 词汇 ---- + +export type UINodeType = + | 'section' | 'list' | 'kv' | 'form' | 'markdown' | 'doc-ref' | 'actions' | 'chart-lite'; + +export interface UINode { + type: UINodeType; + title?: string; + items?: string[]; + kv?: [string, string][]; + fields?: UIField[]; + markdown?: string; + docId?: ID; + actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger' }[]; + chart?: { kind: 'bar' | 'line'; labels: string[]; series: number[] }; + children?: UINode[]; +} + +export interface UIField { + key: string; label: string; + type: 'text' | 'textarea' | 'number' | 'boolean' | 'select'; + options?: string[]; required?: boolean; placeholder?: string; help?: string; default?: string; +} + +// ---- 画像 / 设置 ---- + +export interface AgentProfile { + id: string; name: string; description?: string; builtin?: boolean; source?: string; + systemPrompt: string; + model: { chat?: string; lightweight?: string; embedding?: string; rerank?: string }; + tools: { allow: string[]; deny?: string[]; confirm?: string[] }; + context: { + maxHistoryMessages: number; summarizeBeyond?: boolean; + memoryBudgetPct?: number; knowledgeBudgetPct?: number; maxToolResultChars?: number; + }; + hitl: { askOnAmbiguity: boolean; timeoutSec?: number | null }; + budget: { maxTokens?: number | null; maxToolCalls?: number | null; maxTurns?: number | null }; +} + +export interface SettingDesc { + key: string; group: string; label: string; description?: string; + type: 'string' | 'number' | 'boolean' | 'enum' | 'secret'; + default?: unknown; options?: string[]; + effect: 'hot' | 'new-run' | 'restart'; + secret?: boolean; +} + +// ---- 插件 ---- + +export interface PluginManifest { + id: string; name: string; version: string; apiVersion: string; + requires?: { network?: string[] }; + provides: { + tools?: string[]; workflows?: string[]; intents?: string[]; + prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[]; + }; +} + +export interface PluginPanel { + id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string; +} + +// ---- 摄取 / 检索 ---- + +export interface ParsedDocument { + meta: { + filename: string; mime?: string; parser: string; confidence?: number; + crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' }; + parsedAt: string; + }; + blocks: { + type: 'heading' | 'para' | 'list' | 'table' | 'code' | 'image-ref'; + level?: number; text?: string; items?: string[]; + rows?: string[][]; ref?: string; confidence?: number; + }[]; + warnings?: string[]; +} + +export interface RetrievalHit { + doc: { id: ID; projectId?: ID; source: string; title: string; ref?: string; chunk: string; createdAt: string }; + score: number; kind: 'fulltext' | 'vector' | 'plugin'; +} diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..8cf574d --- /dev/null +++ b/ui.go @@ -0,0 +1,54 @@ +package core + +// UI schema 词汇(4.6 原则 3):设置界面、插件面板、HITL 表单、插件卡片共用 +// 这套有限词汇,由前端通用渲染器渲染。插件不注入 React 组件;表达不了的高级 +// 交互走 Core 契约扩展流程新增词汇,而不是开洞。 + +type UINodeType string + +const ( + UISection UINodeType = "section" // 标题 + 子节点 + UIList UINodeType = "list" // 字符串列表 + UIKV UINodeType = "kv" // 键值对列表 + UIForm UINodeType = "form" // 字段表单(HITL form / 设置分区复用) + UIMarkdown UINodeType = "markdown" + UIDocRef UINodeType = "doc-ref" // 工件引用(右栏查看器打开) + UIActions UINodeType = "actions" // 可点动作(发命令/发消息) + UIChartLite UINodeType = "chart-lite" +) + +type UINode struct { + Type UINodeType `json:"type"` + Title string `json:"title,omitempty"` + Items []string `json:"items,omitempty"` // list + KV [][2]string `json:"kv,omitempty"` // kv + Fields []UIField `json:"fields,omitempty"` // form + Markdown string `json:"markdown,omitempty"` + DocID ID `json:"docId,omitempty"` // doc-ref + Actions []UIAction `json:"actions,omitempty"` + Chart *UIChart `json:"chart,omitempty"` + Children []UINode `json:"children,omitempty"` // section +} + +type UIField struct { + Key string `json:"key"` + Label string `json:"label"` + Type string `json:"type"` // text | textarea | number | boolean | select + Options []string `json:"options,omitempty"` + Required bool `json:"required,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Help string `json:"help,omitempty"` + Default string `json:"default,omitempty"` +} + +type UIAction struct { + ID string `json:"id"` + Label string `json:"label"` + Style string `json:"style,omitempty"` // primary | default | danger +} + +type UIChart struct { + Kind string `json:"kind"` // bar | line + Labels []string `json:"labels"` + Series []float64 `json:"series"` +}