From 9929d8f8adf4b73519ae3eca7956e9dc95cba05d Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 29 Sep 2026 17:34:58 +0000 Subject: [PATCH] Define the core contracts: domain model Details (original message): Define the core contracts: domain model, stream events, HITL, profiles, tools, plugins, UI vocabulary, ingest IR, retrieval and memory; golden-sample conformance tests; TS mirror. --- README.md | 24 ++- conformance_test.go | 179 +++++++++++++++++++ data.go | 146 +++++++++++++++ events.go | 122 +++++++++++++ go.mod | 3 + hitl.go | 45 +++++ model.go | 217 +++++++++++++++++++++++ plugin.go | 69 +++++++ profile.go | 205 +++++++++++++++++++++ testdata/golden/artifact.json | 10 ++ testdata/golden/conversation.json | 7 + testdata/golden/event_agent_spawn.json | 13 ++ testdata/golden/event_message_delta.json | 12 ++ testdata/golden/event_run_completed.json | 17 ++ testdata/golden/hitl_card.json | 26 +++ testdata/golden/message.json | 45 +++++ testdata/golden/parsed_document.json | 40 +++++ testdata/golden/plugin_manifest.json | 28 +++ testdata/golden/profile.json | 32 ++++ testdata/golden/project.json | 6 + testdata/golden/run.json | 17 ++ testdata/golden/tool_http.json | 23 +++ tools.go | 49 +++++ ts/types.ts | 198 +++++++++++++++++++++ ui.go | 54 ++++++ 25 files changed, 1585 insertions(+), 2 deletions(-) create mode 100644 conformance_test.go create mode 100644 data.go create mode 100644 events.go create mode 100644 go.mod create mode 100644 hitl.go create mode 100644 model.go create mode 100644 plugin.go create mode 100644 profile.go create mode 100644 testdata/golden/artifact.json create mode 100644 testdata/golden/conversation.json create mode 100644 testdata/golden/event_agent_spawn.json create mode 100644 testdata/golden/event_message_delta.json create mode 100644 testdata/golden/event_run_completed.json create mode 100644 testdata/golden/hitl_card.json create mode 100644 testdata/golden/message.json create mode 100644 testdata/golden/parsed_document.json create mode 100644 testdata/golden/plugin_manifest.json create mode 100644 testdata/golden/profile.json create mode 100644 testdata/golden/project.json create mode 100644 testdata/golden/run.json create mode 100644 testdata/golden/tool_http.json create mode 100644 tools.go create mode 100644 ts/types.ts create mode 100644 ui.go 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"` +}