Compare commits

...
Author SHA1 Message Date
weichen 38fe861bca Clarify legacy personal settings compatibility
Why: An empty setting scope must not imply shared-project or system authority.

Evidence: Documentation-only clarification of the verified scope contract.

Impact: No runtime change; retain fail-closed shared settings semantics.

Refs: P1, docs/records/2026-10-14-rdm-settings-completion-contract.md
2026-10-14 02:28:23 +00:00
weichen fa9108e2d1 Declare shared and personal settings ownership
Why: project business policy must be distinct from account and per-project private preferences.

Evidence: existing Core contract suite passed (SELF); consumer generation and exact combination checks remain pending.

Impact: additive settings scope vocabulary; omitted scope retains personal compatibility.

Refs: approved settings increment P1/P2; Dev feature/rdm-settings-completion
2026-10-14 01:16:10 +00:00
weichen 5773e74086 Merge pull request 'Add managed scope to authoritative SettingDesc TS mirror' (#32) from fix/settingdesc-managed-scope into main 2026-10-13 06:54:12 +00:00
weichen d6f62af0ed Add managed scope to authoritative SettingDesc TS mirror
Why: Dev 组合构建经 make contracts 从 Core ts/types.ts 重新生成 frontend/ui 的 contracts.ts——受管 scope 字段此前只改了 frontend 镜像,构建时被权威源覆盖丢失(TS2339)。
Evidence: go test ./... 全绿;生成链路验证随 Dev 组合 CI。
Impact: 权威 TS 类型加可选字段;Go 契约(SettingDesc.Scope)此前已随 Core #30 合入,两侧对齐。
2026-10-13 06:53:42 +00:00
weichen 0f94e4b062 Merge pull request 'Add system-managed setting scope contract to SettingDesc' (#30) from feature/rdm-increment-settings-duties into main 2026-10-13 04:19:40 +00:00
weichen aced665ba6 Merge origin/main into feature/rdm-increment-settings-duties 2026-10-13 04:19:24 +00:00
weichen 9150e6f90b Add system-managed setting scope contract to SettingDesc
Why: P2 受管设置机制需要 schema 声明写入归属——system 受管项仅管理员可写,user/project 层拒绝写入且生效链忽略存量覆盖。
Evidence: go test ./... 全绿(ManagedPaths/Touches/Strip 含祖先标量覆盖与整棵子树剥离用例)。
Impact: 新增 SettingScope 常量、SettingDesc.Scope 字段与三个纯函数;空 Scope 保持既有行为,向后兼容。
Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2
2026-10-13 03:40:25 +00:00
weichen 8e4f4e0382 Merge pull request 'Add U1 task summary body contract helpers' (#29) from feature/fast-slow-display-u1 into main
Reviewed-on: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/29
2026-10-12 12:16:33 +00:00
weichenandCursor ba289e7b17 Add U1 task summary body contract helpers
Why: Freeze L1 skeleton KV keys and honest no-findings placeholder
so Backend can stop stuffing raw task text into progress cards.
Evidence: go test ./... -count=1
Impact: New TaskSummaryView helpers; findings[] still deferred to U2/U3.
Refs: fast-slow-channel-display-u1-plan W1

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 07:41:05 +00:00
weichen 89f3906c39 Merge pull request 'Add surface.updated to the StreamEventType union in the TS authority' (#28) from feature/generative-ui-u4-eventtype into main 2026-10-11 11:37:16 +00:00
weichen bbebd153f4 Merge remote-tracking branch 'origin/main' into feature/generative-ui-u4-eventtype 2026-10-11 11:37:11 +00:00
weichen a563d31b1e Add surface.updated to the StreamEventType union in the TS authority
Why: U4 frontend store switches on the event type; the TS union mirrors
the Go EvSurfaceUpdated constant (Core #25) and must include it or the
frontend typecheck rejects the case.

Evidence: go test unchanged (TS-only); frontend tsc in the Dev
workspace after mirror sync.

Impact: Type-only additive.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:36:33 +00:00
weichen 39632c2e7a Merge pull request 'Add the Surface snapshot types to the TS contract authority' (#27) from feature/generative-ui-u4-ts into main 2026-10-11 11:32:55 +00:00
weichen 37b96fd94f Add the Surface snapshot types to the TS contract authority
Why: U4 needs the frontend to render saved surfaces; the Go contract
(Core #25) gains its TypeScript mirror in the single authority so make
contracts regenerates the frontend and UI copies.

Evidence: Mirror regeneration and combined make test in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:32:43 +00:00
weichen 7a98a7e7fe Merge pull request 'Add the optional node id/source fields to the TS contract authority' (#26) from feature/generative-ui-u2-ts-nodeids into main 2026-10-11 08:44:13 +00:00
weichen 360a8a8946 Add the optional node id/source fields to the TS contract authority
Why: PR #25 added the fields to the Go UINode; ts/types.ts is the
single authority that make contracts mirrors into the frontend and UI
repos, so the fields must originate here or the sync reverts manual
mirror edits (CI build caught exactly that).

Evidence: Regenerated mirrors and combined make test run in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u2
2026-10-11 08:44:00 +00:00
weichen 452daf3ffa Merge pull request 'Add the Surface contract types, node ids, and spec validation' (#25) from feature/generative-ui-u2-surface into main 2026-10-11 08:15:26 +00:00
weichen 626f8e1062 Add the Surface contract types, node ids, and spec validation
Why: The Generative UI proposal U2 needs a versioned, host-validated
envelope for agent-authored pages before any backend surface exists:
identity/compat/version envelopes, stable per-node ids with source
annotations, catalog-driven spec validation, and the surface.updated
projection event.

Evidence: go vet + go test in this repo (table tests cover a realistic
valid spec from the U1 trial shape, violations, limits, and old-payload
backward compatibility).

Impact: Additive only - UINode gains optional id/source (omitempty),
old card/form paths unaffected; no existing symbol changes.

Refs: Documents proposal 2026-10-10-generative-ui-plugin.md (r2, U2/U1
sections); Dev task feature/generative-ui-u2
2026-10-11 08:15:10 +00:00
weichen a253a40b8e Merge pull request 'Add the usage split fields to the TypeScript contract authority (N3 WP6)' (#24) from feature/n3-usage-fields-ts into main
Add the usage split fields to the TypeScript contract authority (N3 WP6)
2026-10-07 17:41:26 +00:00
weichen b8913dca6a Add the usage split fields to the TypeScript contract authority (N3 WP6)
Why: frontend and ui contracts.ts are generated from this file by
make contracts; editing the generated copies directly gets reverted by
the next codegen run (the Dev CI build step proves it regenerates).
Evidence: GOWORK=off go test green; the shape matches the Go Usage
fields added earlier.
Impact: additive to the contract mirror.
Refs: Agent-System-Core#2, Agent-System-Frontend#48
2026-10-07 17:41:21 +00:00
weichen 1b7e2c0f47 Merge pull request 'Add last-request and cache fields to Usage (N3 WP6)' (#23) from feature/n3-usage-fields into main
Add last-request and cache fields to Usage (N3 WP6)
2026-10-07 17:04:22 +00:00
weichen 8a3ff92518 Add last-request and cache fields to Usage (N3 WP6)
Why: the loop now emits Last* and cache token usage; the product run
record needs the same fields for display and accounting (additive JSON,
old records deserialize as zero values).
Evidence: GOWORK=off go build + go test green.
Impact: additive only.
Refs: Agent-Native#35, N3 plan WP6
2026-10-07 16:34:27 +00:00
weichen f4798ac852 Merge pull request 'Document executable Core verification commands' (#22) from feature/governance-docs into main 2026-10-06 03:09:54 +00:00
weichen cf2615def6 Document executable Core verification commands
Why: project facts pointed to a nonexistent Makefile and retained obsolete skeleton status.

Evidence: SELF go test ./... and git diff --check completed in this component worktree.

Impact: documentation only; does not advance the Dev Core gitlink or change public contracts.

Refs: Agent-System-Dev feature/governance-reliability audit remediation
2026-10-05 12:29:42 +00:00
weichen b12f9e29e6 Extend messages and context observation for variants
Merge pull request 'Extend messages and context observation for variants' (#21) from feature/sillytavern-review into main
2026-10-04 19:44:55 +00:00
weichen 4eae71864c Merge remote-tracking branch 'origin/main' into feature/sillytavern-review 2026-10-04 19:42:53 +00:00
weichenandCursor 0b5c7747a3 Keep the context section type inside the contract file
Why: A separately exported ContextSection is unused inside the UI package and pushes the knip ratchet over its baseline.
Evidence: UI knip reported 56 unused exports against baseline 55. The section shape stays on ContextObservation.sections.
Impact: Generated frontend and UI contracts drop the extra export. JSON field names are unchanged.
Refs: .task/feature-sillytavern-review.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 18:36:08 +00:00
weichen bd632236c2 Extend messages and context observation for variants and estimates
Why: Labeled secrets, excluded turns, and candidate replies need fields on the shared message contract, and context snapshots need a section breakdown plus a stable prefix hash.
Evidence: go test ./... in agent-system-core. New fields use omitempty, so existing golden JSON stays the same.
Impact: Clients that ignore unknown fields keep working. Assemblers can now mark a message excluded or as one candidate in a group.
Refs: docs/records/2026-10-04-sillytavern-review.md
2026-10-04 18:18:57 +00:00
weichen a7db6a4d0f Merge pull request 'Record the model that served a run's usage' (#20) from feature/durable-rewind into main 2026-10-03 17:40:15 +00:00
weichenandCursor 5dc190ee63 Record the model that served a run's usage
Why: a fallback chain can answer with a different model than the one that was requested, and usage has to name the model that actually served.
Evidence: go test .
Impact: the field is omitted when empty, so existing usage JSON stays the same.
Refs: feature/durable-rewind
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:28:40 +00:00
weichen b7e5baf15c Merge pull request 'Add session permission mode and confirmation detail' (#19) from feature/permission-hooks into main 2026-10-03 17:06:48 +00:00
weichenandCursor cf7df54874 Add session permission mode and confirmation detail
Why: The product needs a session mode and a place to show tool arguments on a confirmation card without inventing a second card type.
Evidence: GOWORK=off go test . passed. New fields use omitempty, so existing golden JSON stays the same.
Impact: Empty permission mode still means the profile policy. Clients that ignore unknown fields keep working.
Refs: feature/permission-hooks
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:02:28 +00:00
weichen 88e481afcd Merge pull request 'Add a finalizing phase and a check part' (#18) from feature/final-reply-compose into main 2026-10-02 14:15:05 +00:00
weichenandCursor 7c66075ee9 Add a finalizing phase and a check part
Why: The final reply is composed after the work loop, and the automatic end check needs its own part and event so the timeline can show it without mixing it into the user-facing text.
Evidence: go test ./...
Impact: New RunPhase finalizing, PartType check, and run.check. Existing payloads are unchanged.
Refs: feature/final-reply-compose
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 13:32:32 +00:00
weichen 3f7067fb2d Merge pull request 'Add streaming events for in-flight tool calls' (#17) from feature/stream-display-overhaul into main 2026-10-02 05:04:38 +00:00
weichenandCursor f08a4a90c6 Add streaming events for in-flight tool calls
Why: The client only learned about a tool call once execution started, so long argument generation looked like a frozen cursor.
Evidence: go test ./...
Impact: New tool.call.delta and tool.output events; tool.result may include durationMs. Unknown event types stay ignored.
Refs: feature/stream-display-overhaul
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 02:11:42 +00:00
weichen 4d1123e45c Merge pull request 'Add script steps and critic fields to workflow definitions' (#16) from feature/agent-harness-p4 into main
Merge pull request #16
2026-10-02 01:03:55 +00:00
weichenandCursor f93f473a0e Add script steps and critic fields to workflow definitions
Why: The workflow executor needs a shared contract for subagent profiles, plugin scripts, and per-step plus whole-run retry caps.
Evidence: go test ./... passed, including the new workflow_def golden.
Impact: New fields are omitempty, so existing workflow JSON still decodes. Kind script is additive.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:55:52 +00:00
weichen a8e81e078d Merge pull request 'Add optional tools, model, and maxTurns to prompt modules' (#15) from feature/agent-harness-p3b into main 2026-10-01 15:51:54 +00:00
16 changed files with 1169 additions and 117 deletions
+9 -2
View File
@@ -2,5 +2,12 @@
- 用途:见 README。定位与边界见设计文档(Agent-System-Documents)。
- 技术栈:Go(静态链接,CGO_ENABLED=0)
- 构建/验证:见仓内 Makefile 或 Dev 仓顶层 Makefile(待 P1 起补充并保持本节同步)。
- 状态:P0 骨架;当前阶段见 Dev 仓任务记录。
## 验证
在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行
`git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。
修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test`
验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
+20
View File
@@ -207,6 +207,26 @@ func TestGoldenIngest(t *testing.T) {
})
}
func TestGoldenWorkflowDef(t *testing.T) {
golden(t, "workflow_def", WorkflowDef{
ID: "demo.brief", Title: "简报", Description: "按主题写一份简报",
IntentID: "demo.brief", MaxRetries: 6,
Steps: []WorkflowStep{
{ID: "length", Title: "篇幅", Kind: "hitl", Hitl: &HITLCardTemplate{
Question: "篇幅?", Kind: HITLKindChoice, AllowFreeInput: false,
Choices: []HITLChoice{{ID: "short", Label: "短", Description: "一页", Impact: "更快"}},
}},
{ID: "draft", Title: "起草", Kind: "prompt", Agent: "research",
Prompt: "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
Critic: &StepCritic{Criteria: "必须点明主题", MaxRetries: 1}},
{ID: "lookup", Title: "查时间", Kind: "tool", Tool: "time_now", Args: json.RawMessage(`{"tz":"UTC"}`)},
{ID: "save", Title: "存稿", Kind: "artifact", Artifact: "brief.md", Prompt: "{{steps.draft.output}}"},
{ID: "note", Title: "通知", Kind: "card", Card: &ActivityCardTemplate{Kind: CardKindResult, Title: "简报已写好"}},
{ID: "run", Title: "脚本", Kind: "script", Script: "scripts/demo.py"},
},
})
}
func TestGoldenPlugin(t *testing.T) {
golden(t, "plugin_manifest", PluginManifest{
ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion,
+51 -13
View File
@@ -8,13 +8,13 @@ import (
// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。
// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。
// 项目流(D50):项目频道事件以伪会话键 "project:<projectId>" 复用同一机制
//(独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
// (独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
type StreamEvent struct {
Seq int64 `json:"seq"`
RunID ID `json:"runId,omitempty"`
ConversationID ID `json:"conversationId"`
ProjectID ID `json:"projectID,omitempty"` // 项目流事件的项目归属(D50;只增不删)
TS string `json:"ts"` // RFC3339
TS string `json:"ts"` // RFC3339
V int `json:"v"`
Type StreamEventType `json:"type"`
Payload json.RawMessage `json:"payload,omitempty"`
@@ -38,9 +38,12 @@ func ProjectFromChannel(key ID) (ID, bool) {
const (
EvMessageDelta StreamEventType = "message.delta"
EvThinkingDelta StreamEventType = "thinking.delta"
EvToolCallDelta StreamEventType = "tool.call.delta" // 参数增量:工具开始执行前即可上屏
EvToolCall StreamEventType = "tool.call"
EvToolOutput StreamEventType = "tool.output" // 执行中的 stdout/stderr 增量
EvToolResult StreamEventType = "tool.result"
EvRunPhase StreamEventType = "run.phase"
EvRunCheck StreamEventType = "run.check" // 终轮自动检查,过程区一行
EvHITLRequest StreamEventType = "hitl.request"
EvHITLResolved StreamEventType = "hitl.resolved"
EvPlanUpdate StreamEventType = "plan.update"
@@ -58,6 +61,9 @@ const (
EvCardUpdated StreamEventType = "card.updated"
EvCardStatus StreamEventType = "card.status"
EvCardResolved StreamEventType = "card.resolved"
// EvSurfaceUpdated:Generative UI Surface 快照保存后的投影事件(提案 U2;
// 载荷 core.SurfaceSnapshot,按会话流发布,前端消费在 U4)。
EvSurfaceUpdated StreamEventType = "surface.updated"
)
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
@@ -82,17 +88,40 @@ type ToolCallPayload struct {
Arguments string `json:"arguments,omitempty"`
}
// ToolCallDeltaPayload 是模型正在编写的工具参数增量。Index 标识同轮第几次调用
// (与供应商 tool_calls 下标一致);Name/CallID 往往只在首个分片出现。
type ToolCallDeltaPayload struct {
Index int `json:"index"`
CallID string `json:"callId,omitempty"`
Name string `json:"name,omitempty"`
Delta string `json:"delta"`
}
// ToolOutputPayload 是工具执行过程中的输出增量。Stream 为 stdout 或 stderr。
type ToolOutputPayload struct {
CallID string `json:"callId"`
Stream string `json:"stream"`
Text string `json:"text"`
}
type ToolResultPayload struct {
CallID string `json:"callId"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Truncated bool `json:"truncated,omitempty"`
CallID string `json:"callId"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Truncated bool `json:"truncated,omitempty"`
DurationMs float64 `json:"durationMs,omitempty"`
}
type RunPhasePayload struct {
Phase RunPhase `json:"phase"`
}
// RunCheckPayload 与 CheckPart 同形,让运行中和重新加载后的检查行一致。
type RunCheckPayload struct {
Passed bool `json:"passed"`
Reason string `json:"reason,omitempty"`
}
type HITLResolvedPayload struct {
CardID ID `json:"cardId"`
Answer string `json:"answer,omitempty"`
@@ -115,15 +144,15 @@ type AgentSpawnPayload struct {
}
type AgentProgressPayload struct {
AgentRun ID `json:"agentRun"`
Note string `json:"note,omitempty"`
AgentRun ID `json:"agentRun"`
Note string `json:"note,omitempty"`
Phase RunPhase `json:"phase,omitempty"`
}
type AgentResultPayload struct {
AgentRun ID `json:"agentRun"`
AgentRun ID `json:"agentRun"`
Status RunStatus `json:"status"`
Summary string `json:"summary,omitempty"`
Summary string `json:"summary,omitempty"`
// Text 为子代理完整文本产出(squad 步骤交付物用;截断摘要见 Summary)。
Text string `json:"text,omitempty"`
}
@@ -138,11 +167,20 @@ type RunCompletedPayload struct {
Context *ContextObservation `json:"context,omitempty"`
}
// ContextSection 是组装结果里一段的估算。EstTokens 用 UTF-8 字节数 / 3.35,
// 再按上一请求的真实用量校准。
type ContextSection struct {
Name string `json:"name"`
EstTokens int `json:"estTokens"`
}
// ContextObservation 记录一次模型输入的实际构成与预算裁剪(ctxasm 产出)。
type ContextObservation struct {
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
EstChars int `json:"estChars"` // 估算输入字符量
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
EstChars int `json:"estChars"` // 估算输入量(token;字段名保持兼容)
Sections []ContextSection `json:"sections,omitempty"`
PrefixHash string `json:"prefixHash,omitempty"` // 系统前缀哈希,前缀不变时保持稳定
}
type RunFailedPayload struct {
+2
View File
@@ -23,6 +23,8 @@ type HITLCard struct {
// Blocked:阻断态(门禁不通过等)——确认入口禁用并给出出路;阻断本身进审计。
Blocked bool `json:"blocked,omitempty"`
BlockReason string `json:"blockReason,omitempty"`
// Detail 是确认卡上的参数预览或其他补充说明。
Detail string `json:"detail,omitempty"`
}
type HITLKind string
+90 -64
View File
@@ -17,26 +17,30 @@ type ID = string
// Owner 是租户标识(多租户主线,设计 D5/D16):空串 = 单租户默认(嵌入式档/
// 未启用认证时的隐式 owner)。API 层强制 owner 过滤,跨租户访问一律 NotFound。
type Project struct {
ID ID `json:"id"`
Owner ID `json:"owner,omitempty"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID ID `json:"id"`
Owner ID `json:"owner,omitempty"`
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"`
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
Title string `json:"title"`
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID ID `json:"id"`
ProjectID ID `json:"projectId"`
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
Title string `json:"title"`
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
// PermissionMode 是会话权限:空或 default 沿用画像;plan 只读加计划工具;auto_edit 放行工作区写文件。
PermissionMode string `json:"permissionMode,omitempty"`
// SessionNote 是本会话要求,组装时附在上下文末尾,不进入系统前缀。
SessionNote string `json:"sessionNote,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ---- 消息与部件 ----
@@ -61,16 +65,22 @@ const (
PartHITL PartType = "hitl"
PartPlan PartType = "plan"
PartError PartType = "error"
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
PartCheck PartType = "check" // 终轮自动检查,过程区一行,不是给用户的回复
)
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"`
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"`
// Excluded 为真时界面仍显示,但组装上下文时整条(及同一 run 的成组消息)不送入模型。
Excluded bool `json:"excluded,omitempty"`
// VariantGroup 把同一轮的候选回复绑在一起。只把未排除的那条送入模型。
VariantGroup string `json:"variantGroup,omitempty"`
VariantIndex int `json:"variantIndex,omitempty"`
}
// Part 是判别联合:Type 决定哪些字段有意义;未用字段为零值。
@@ -84,8 +94,9 @@ type Part struct {
Artifact *ArtifactRef `json:"artifact,omitempty"`
Hitl *HITLSummary `json:"hitl,omitempty"`
Plan *PlanSnapshot `json:"plan,omitempty"`
Card *CardPart `json:"card,omitempty"`
Card *CardPart `json:"card,omitempty"`
Err *ErrorPart `json:"error,omitempty"`
Check *CheckPart `json:"check,omitempty"`
// 过程标记:true 的部件在 run 终态后由前端折叠为摘要行(4.2)。
Process bool `json:"process,omitempty"`
@@ -96,17 +107,17 @@ type Part struct {
}
type ToolCallPart struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments string `json:"arguments,omitempty"` // 原始 JSON
Duration float64 `json:"durationMs,omitempty"`
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)
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 {
@@ -127,6 +138,12 @@ type ErrorPart struct {
Message string `json:"message"`
}
// CheckPart 是终轮自动检查的结果。过程区显示一行,不进入给用户的回复。
type CheckPart struct {
Passed bool `json:"passed"`
Reason string `json:"reason,omitempty"`
}
// CardPart.Data 是 UI schema 节点(4.6 有限词汇)。
// CardID 非空 = 收件箱 ActivityCard 的引用部件(如 submit_card 产卡:卡片为产物
// 权威载体,消息只存引用+锚点渲染);空 = 插件内嵌卡片部件(自包含 Data)。
@@ -149,39 +166,40 @@ const (
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"
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"
PhaseThinking RunPhase = "thinking"
PhaseActing RunPhase = "acting"
PhaseResponding RunPhase = "responding"
PhaseDone RunPhase = "done"
PhaseFinalizing RunPhase = "finalizing" // 工作循环已结束,正在整理给用户的最终回复
PhaseDone RunPhase = "done"
)
type Run struct {
ID ID `json:"id"`
ParentRunID ID `json:"parentRunId,omitempty"`
ConversationID ID `json:"conversationId"`
ProjectID ID `json:"projectId"`
Owner ID `json:"owner,omitempty"` // 冗余自会话
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"`
ID ID `json:"id"`
ParentRunID ID `json:"parentRunId,omitempty"`
ConversationID ID `json:"conversationId"`
ProjectID ID `json:"projectId"`
Owner ID `json:"owner,omitempty"` // 冗余自会话
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 {
@@ -190,6 +208,14 @@ type Usage struct {
TotalTokens int `json:"totalTokens"`
DurationMs float64 `json:"durationMs"`
ToolCalls int `json:"toolCalls"`
// 以下四项是 N3 的计费拆分(additive;旧记录为零值省略):
// Last* 是最近一次请求的用量(上一请求),Cache* 是缓存命中累计。
LastPromptTokens int `json:"lastPromptTokens,omitempty"`
LastCompletionTokens int `json:"lastCompletionTokens,omitempty"`
CacheReadTokens int `json:"cacheReadTokens,omitempty"`
CacheWriteTokens int `json:"cacheWriteTokens,omitempty"`
// Model 是实际给出这段用量的模型。空表示当时没有记录。
Model string `json:"model,omitempty"`
}
// ---- 工件 ----
@@ -210,20 +236,20 @@ type Artifact struct {
// ---- 计划与待办(右栏 Plan / Todos)----
type PlanSnapshot struct {
PlanID ID `json:"planId"`
Title string `json:"title"`
Steps []PlanStep `json:"steps"`
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"`
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"`
ConversationID ID `json:"conversationId"`
Items []TodoItem `json:"items"`
}
+12 -1
View File
@@ -52,24 +52,35 @@ type PluginPanel struct {
}
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
// 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。
type WorkflowDef struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
MaxRetries int `json:"maxRetries,omitempty"` // 整条工作流的 critic 重试上限
Steps []WorkflowStep `json:"steps"`
}
// StepCritic:单步验收。无法解析的判定按不通过处理。
type StepCritic struct {
Criteria string `json:"criteria"`
MaxRetries int `json:"maxRetries,omitempty"` // 本步重试上限;0 表示缺省 1 次
}
type WorkflowStep struct {
ID string `json:"id"`
Title string `json:"title"`
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card | script
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
Agent string `json:"agent,omitempty"` // prompt: 子代理 profile,缺省 general
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: 产出工件名模板
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
Script string `json:"script,omitempty"` // script: 插件目录内的相对路径
Critic *StepCritic `json:"critic,omitempty"`
}
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
+150 -20
View File
@@ -1,45 +1,48 @@
package core
import "encoding/json"
import (
"encoding/json"
"strings"
)
// 智能体画像(4.7):核心定制机制,全部数据化、可导入导出。
type AgentProfile struct {
ID string `json:"id"`
Owner string `json:"owner,omitempty"` // 租户;空 = 共享(builtin)
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"`
ID string `json:"id"`
Owner string `json:"owner,omitempty"` // 租户;空 = 共享(builtin)
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"`
// PromptModules 引用提示词库模块 ID(有序;空 = 组装默认基座全集)。
// SystemPrompt 兼容保留:非空时作为附加段追加在模块组装结果之后。
PromptModules []string `json:"promptModules,omitempty"`
Model ModelRoute `json:"model"`
Tools ToolPolicy `json:"tools"`
Context ContextPolicy `json:"context"`
HITL HITLPolicy `json:"hitl"`
Budget RunBudget `json:"budget"`
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"`
Chat string `json:"chat,omitempty"`
Lightweight string `json:"lightweight,omitempty"`
Embedding string `json:"embedding,omitempty"`
Rerank string `json:"rerank,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"` // ["*"] 全部
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
MaxHistoryMessages int `json:"maxHistoryMessages"` // 0 = 默认 40
SummarizeBeyond bool `json:"summarizeBeyond,omitempty"` // 远期;先存标志
MemoryBudgetPct int `json:"memoryBudgetPct,omitempty"` // 注入槽份额上限
KnowledgeBudgetPct int `json:"knowledgeBudgetPct,omitempty"`
@@ -102,11 +105,27 @@ const (
type SettingEffect string
const (
EffectHot SettingEffect = "hot" // 立即生效
EffectNewRun SettingEffect = "new-run" // 仅影响新 run
EffectHot SettingEffect = "hot" // 立即生效
EffectNewRun SettingEffect = "new-run" // 仅影响新 run
EffectRestart SettingEffect = "restart" // 需重启
)
// SettingScope 声明设置项的写入归属(提案 2026-10-13-rdm-settings-members-duties-perms-increment P2)。
// 空串保留旧个人配置兼容,不表示共享项目策略或系统管理资格。
type SettingScope string
const (
// SettingScopeSystem 受管项:仅系统管理面(admin)可写;user/project 写入被拒,
// 生效链忽略这两层的存量覆盖值。
SettingScopeSystem SettingScope = "system"
// SettingScopeUser is a private account preference, never a shared project policy.
SettingScopeUser SettingScope = "user"
// SettingScopeProject is shared business configuration under project authority.
SettingScopeProject SettingScope = "project"
// SettingScopeProjectPersonal is a private preference within one project.
SettingScopeProjectPersonal SettingScope = "project-personal"
)
// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。
type SettingDesc struct {
Key string `json:"key"`
@@ -118,6 +137,117 @@ type SettingDesc struct {
Options []string `json:"options,omitempty"` // enum
Effect SettingEffect `json:"effect"`
Secret bool `json:"secret,omitempty"`
Scope SettingScope `json:"scope,omitempty"` // Empty is legacy personal compatibility; explicit scopes own their write surface.
}
// ManagedPaths 返回 schema 中 scope=system 的键路径(点路径形式)。
func ManagedPaths(descs []SettingDesc) []string {
out := make([]string, 0, len(descs))
for _, d := range descs {
if d.Scope == SettingScopeSystem {
out = append(out, d.Key)
}
}
return out
}
// FlattenSettingLeafPaths 列出 values 的全部叶子点路径。子树为标量或数组时该子树
// 根即叶子。用于受管触碰检测:深合并语义下叶子决定最终覆盖面。
func FlattenSettingLeafPaths(values map[string]any) []string {
var out []string
var walk func(prefix string, m map[string]any)
walk = func(prefix string, m map[string]any) {
for k, v := range m {
path := k
if prefix != "" {
path = prefix + "." + k
}
if vm, ok := v.(map[string]any); ok && len(vm) > 0 {
walk(path, vm)
continue
}
out = append(out, path)
}
}
walk("", values)
return out
}
// pathTouchesManaged 判断 path 是否触碰 managed:等于受管路径、位于受管子树内
// (受管路径是 path 的前缀),或整体覆盖受管子树(path 是受管路径的前缀——
// 标量覆盖整棵受管 map 同样破坏受管值)。
func pathTouchesManaged(path string, managed []string) bool {
for _, mp := range managed {
if path == mp ||
strings.HasPrefix(path, mp+".") ||
strings.HasPrefix(mp, path+".") {
return true
}
}
return false
}
// TouchesManagedSettings 返回 values 中触碰受管路径的叶子;空切片 = 无触碰。
// 调用方须传入已展开点路径的嵌套 map(normalize 后)。
func TouchesManagedSettings(values map[string]any, managed []string) []string {
var hits []string
for _, p := range FlattenSettingLeafPaths(values) {
if pathTouchesManaged(p, managed) {
hits = append(hits, p)
}
}
return hits
}
// StripManagedSettings 返回删除受管路径后的浅拷贝(不改入参)。受管路径的祖先
// 若因此只剩空 map 一并删除。入参须为嵌套形式(点路径已展开)。
func StripManagedSettings(values map[string]any, managed []string) map[string]any {
if len(values) == 0 || len(managed) == 0 {
return values
}
// 叶子命中或祖先整体命中都删;逐叶子重建最简单可靠。
leaves := FlattenSettingLeafPaths(values)
keep := map[string]any{}
for _, leaf := range leaves {
if pathTouchesManaged(leaf, managed) {
continue
}
setPath(keep, leaf, lookupAny(values, leaf))
}
return keep
}
func setPath(m map[string]any, path string, val any) {
parts := strings.Split(path, ".")
cur := m
for i, p := range parts {
if i == len(parts)-1 {
cur[p] = val
return
}
next, ok := cur[p].(map[string]any)
if !ok {
next = map[string]any{}
cur[p] = next
}
cur = next
}
}
func lookupAny(m map[string]any, path string) any {
parts := strings.Split(path, ".")
var cur any = m
for _, p := range parts {
cm, ok := cur.(map[string]any)
if !ok {
return nil
}
cur, ok = cm[p]
if !ok {
return nil
}
}
return cur
}
// EffectiveSettings 是覆盖链(system → user → project → conversation)深合并结果。
+85
View File
@@ -0,0 +1,85 @@
package core
import (
"reflect"
"sort"
"testing"
)
func TestManagedPathsFiltersSystemScope(t *testing.T) {
descs := []SettingDesc{
{Key: "models.chat.model", Scope: SettingScopeSystem},
{Key: "ui.theme"},
{Key: "tools.tavily.apiKeys", Scope: SettingScopeSystem},
}
got := ManagedPaths(descs)
sort.Strings(got)
want := []string{"models.chat.model", "tools.tavily.apiKeys"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ManagedPaths = %v, want %v", got, want)
}
}
func TestTouchesManagedSettings(t *testing.T) {
managed := []string{"models.chat.model", "tools.tavily.apiKeys", "telemetry.otlpEndpoint"}
cases := []struct {
name string
in map[string]any
want []string
}{
{"exact leaf", map[string]any{"models": map[string]any{"chat": map[string]any{"model": "x"}}}, []string{"models.chat.model"}},
{"sibling untouched", map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": "x"}}}, nil},
{"ancestor scalar overwrite", map[string]any{"models": "oops"}, []string{"models"}},
{"subtree inside managed", map[string]any{"telemetry": map[string]any{"otlpEndpoint": "http://x"}}, []string{"telemetry.otlpEndpoint"}},
{"other group untouched", map[string]any{"ui": map[string]any{"theme": "light"}}, nil},
{"empty map", map[string]any{}, nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := TouchesManagedSettings(tc.in, managed)
sort.Strings(got)
if tc.want == nil && len(got) != 0 {
t.Fatalf("TouchesManagedSettings = %v, want none", got)
}
if tc.want != nil && !reflect.DeepEqual(got, tc.want) {
t.Fatalf("TouchesManagedSettings = %v, want %v", got, tc.want)
}
})
}
}
func TestStripManagedSettings(t *testing.T) {
managed := []string{"models.chat.model", "models.chat.apiKey"}
in := map[string]any{
"models": map[string]any{
"chat": map[string]any{"model": "x", "baseUrl": "https://b"},
"lightweight": map[string]any{"model": "y"},
},
"ui": map[string]any{"theme": "dark"},
}
got := StripManagedSettings(in, managed)
want := map[string]any{
"models": map[string]any{
"chat": map[string]any{"baseUrl": "https://b"},
"lightweight": map[string]any{"model": "y"},
},
"ui": map[string]any{"theme": "dark"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("StripManagedSettings = %v, want %v", got, want)
}
if in["models"].(map[string]any)["chat"].(map[string]any)["model"] != "x" {
t.Fatal("StripManagedSettings 不应修改入参")
}
}
func TestStripManagedSettingsWholeSubtree(t *testing.T) {
managed := []string{"models.chat.model", "models.chat.baseUrl", "models.chat.apiKey", "models.chat.fallbacks"}
in := map[string]any{
"models": map[string]any{"chat": map[string]any{"model": "x", "baseUrl": "b", "apiKey": "k"}},
}
got := StripManagedSettings(in, managed)
if len(got) != 0 {
t.Fatalf("整棵受管子树应被剥除(含空父),got %v", got)
}
}
+224
View File
@@ -0,0 +1,224 @@
package core
import "fmt"
// Surface 契约(Generative UI 提案 U2;依据 r2「U1 结果与 r2 修订」):
// 有版本的呈现产物信封 + 页面规格校验。信封与版本字段由服务端分配/校验,
// 模型只产出 SurfaceSpec;节点级稳定 ID 与来源标注由 UINode 的可选字段承载
// (ui.go),旧卡片/表单路径零影响。
const (
// SurfaceSchemaVersion 是 Surface 信封/规格的结构版本。
SurfaceSchemaVersion = 1
// SurfaceCatalogVersion 是当前认可的组件目录版本(现有 16 类 UINode 词表)。
SurfaceCatalogVersion = "ui-v1"
// SurfaceMaxNodes / SurfaceMaxDepth / SurfaceMaxBytes 是 U1 试验采纳的资源
// 预算(提案 KTD5):超限整页拒绝,不静默裁剪。
SurfaceMaxNodes = 200
SurfaceMaxDepth = 8
SurfaceMaxBytes = 256 * 1024
)
// SurfaceCompat 兼容标识(KTD3):由宿主与已安装组件协商记录,不采信自报值。
type SurfaceCompat struct {
SchemaVersion int `json:"schemaVersion"`
CatalogVersion string `json:"catalogVersion"`
PluginVersion string `json:"pluginVersion,omitempty"`
}
// SurfaceIdentity 身份信封:全部由服务端分配或从当前请求绑定;提交方不能自报。
type SurfaceIdentity struct {
SurfaceID ID `json:"surfaceId"`
Owner string `json:"owner"`
ProjectID ID `json:"projectId,omitempty"`
ConversationID ID `json:"conversationId,omitempty"`
SourceRunID ID `json:"sourceRunId,omitempty"`
PluginID string `json:"pluginId"`
}
// SurfaceSpec 是模型可产出的页面规格(KTD3「页面」层)。
type SurfaceSpec struct {
Title string `json:"title"`
Nodes []UINode `json:"nodes"`
}
// SurfaceSnapshot 是已保存的呈现产物:信封+兼容+版本+页面+回放摘要。
// Revision 单调递增;更新必须携带 BaseRevision,冲突由服务端拒绝(不自动覆盖)。
type SurfaceSnapshot struct {
Identity SurfaceIdentity `json:"identity"`
Compat SurfaceCompat `json:"compat"`
Revision int `json:"revision"`
BaseRevision int `json:"baseRevision,omitempty"`
ContextVersion string `json:"contextVersion,omitempty"`
ActivationEpoch int64 `json:"activationEpoch"`
Spec SurfaceSpec `json:"spec"`
SemanticText string `json:"semanticText,omitempty"`
}
// SurfaceMeta 是列表投影(不携带整页规格)。
type SurfaceMeta struct {
SurfaceID ID `json:"surfaceId"`
Title string `json:"title"`
PluginID string `json:"pluginId"`
Revision int `json:"revision"`
ConversationID ID `json:"conversationId,omitempty"`
UpdatedAt string `json:"updatedAt"`
}
// ValidateSurfaceSpec 按目录纪律校验页面规格,返回违规清单(空即合法)。
// knownSources 为 nil 表示本层不校验来源授权(U3 数据绑定层职责),仅做格式
// 检查;非空时引用/doc-ref 未知来源即为违规。未知类型、非法 props、重复 ID、
// 超限均拒绝(U1 试验校验器的 Go 版本:目录=提示=校验三同源)。
func ValidateSurfaceSpec(spec SurfaceSpec, knownSources map[string]bool) []string {
var errs []string
seen := map[string]bool{}
nodes := 0
var walk func(n UINode, path string, depth int)
walk = func(n UINode, path string, depth int) {
nodes++
if nodes > SurfaceMaxNodes {
errs = append(errs, fmt.Sprintf("%s: 节点数超过 %d", path, SurfaceMaxNodes))
return
}
if depth > SurfaceMaxDepth {
errs = append(errs, fmt.Sprintf("%s: 嵌套深度超过 %d", path, SurfaceMaxDepth))
return
}
if n.ID != "" {
if seen[n.ID] {
errs = append(errs, fmt.Sprintf("%s: 重复节点 id %q", path, n.ID))
}
seen[n.ID] = true
}
for _, s := range n.Source {
if s == "" {
errs = append(errs, path+": source 含空标识")
continue
}
if knownSources != nil && !knownSources[s] {
errs = append(errs, fmt.Sprintf("%s: 引用未知来源 %q", path, s))
}
}
switch n.Type {
case UISection:
for i, c := range n.Children {
walk(c, fmt.Sprintf("%s/child[%d]", path, i), depth+1)
}
case UIList:
if n.Items == nil {
errs = append(errs, path+": list 需要 items")
}
case UIKV:
if n.KV == nil {
errs = append(errs, path+": kv 需要键值对数组")
}
case UIForm:
if len(n.Fields) == 0 {
errs = append(errs, path+": form.fields 需非空")
}
for _, f := range n.Fields {
switch f.Type {
case "text", "textarea", "number", "boolean", "select":
default:
errs = append(errs, fmt.Sprintf("%s: 字段 %q 非法 type %q", path, f.Key, f.Type))
}
if f.Type == "select" && len(f.Options) == 0 {
errs = append(errs, fmt.Sprintf("%s: select 字段 %q 缺 options", path, f.Key))
}
}
case UIMarkdown:
if n.Markdown == "" {
errs = append(errs, path+": markdown 需要正文")
}
case UIDocRef:
if n.DocID == "" {
errs = append(errs, path+": doc-ref 需要 docId")
} else if knownSources != nil && !knownSources[string(n.DocID)] {
errs = append(errs, fmt.Sprintf("%s: doc-ref 引用未知工件 %q", path, n.DocID))
}
case UIActions:
if len(n.Actions) == 0 {
errs = append(errs, path+": actions 需非空")
}
for _, a := range n.Actions {
if a.ID == "" || a.Label == "" {
errs = append(errs, fmt.Sprintf("%s: action 缺 id/label", path))
}
switch a.Style {
case "", "primary", "default", "danger":
default:
errs = append(errs, fmt.Sprintf("%s: action %q 非法 style %q", path, a.ID, a.Style))
}
}
case UIChartLite:
if n.Chart == nil || (n.Chart.Kind != "bar" && n.Chart.Kind != "line") ||
len(n.Chart.Labels) != len(n.Chart.Series) {
errs = append(errs, path+": chart 需要 kind∈{bar,line} 且 labels/series 等长")
}
case UIAvatar:
if n.Avatar == nil || n.Avatar.Name == "" {
errs = append(errs, path+": avatar 需要 name")
}
case UIProgress:
if n.Progress == nil || n.Progress.Value < 0 || n.Progress.Value > 1 {
errs = append(errs, path+": progress.value 需在 0~1")
}
case UICompare:
if n.Compare == nil {
errs = append(errs, path+": compare 需要 left/right")
} else {
for side, sd := range map[string]UICompareSide{"left": n.Compare.Left, "right": n.Compare.Right} {
for i, c := range sd.Nodes {
walk(c, fmt.Sprintf("%s/%s[%d]", path, side, i), depth+1)
}
}
}
case UITree:
if n.Tree == nil || len(n.Tree.Nodes) == 0 {
errs = append(errs, path+": tree.nodes 需非空")
}
case UITable:
if n.Table == nil || len(n.Table.Columns) == 0 {
errs = append(errs, path+": table.columns 需非空")
} else {
for i, r := range n.Table.Rows {
if len(r.Cells) != len(n.Table.Columns) {
errs = append(errs, fmt.Sprintf("%s: 行[%d] cells 与 columns 不等长", path, i))
}
}
}
case UISteps:
if n.Steps == nil || len(n.Steps.Steps) == 0 || n.Steps.Current < 0 || n.Steps.Current >= len(n.Steps.Steps) {
errs = append(errs, path+": steps 需非空且 current 在范围内")
}
case UITabs:
if n.Tabs == nil || len(n.Tabs.Tabs) == 0 {
errs = append(errs, path+": tabs.tabs 需非空")
} else {
for i, tb := range n.Tabs.Tabs {
for j, c := range tb.Nodes {
walk(c, fmt.Sprintf("%s/tab[%d][%d]", path, i, j), depth+1)
}
}
}
case UITimeline:
if n.Timeline == nil || len(n.Timeline.Items) == 0 {
errs = append(errs, path+": timeline.items 需非空")
}
default:
errs = append(errs, fmt.Sprintf("%s: 未知节点类型 %q", path, n.Type))
}
}
if spec.Title == "" {
errs = append(errs, "$: title 必填")
}
if len(spec.Nodes) == 0 {
errs = append(errs, "$: nodes 需非空")
}
for i, n := range spec.Nodes {
walk(n, fmt.Sprintf("$[%d]", i), 1)
}
return errs
}
+137
View File
@@ -0,0 +1,137 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
func validSpec() SurfaceSpec {
return SurfaceSpec{
Title: "三家候选工具选型对比",
Nodes: []UINode{
{Type: UISection, ID: "n1", Title: "一、概览", Children: []UINode{
{Type: UIMarkdown, ID: "n1a", Source: []string{"doc:eval/rc-01/vendor1"}, Markdown: "本期评估三家候选。"},
}},
{Type: UITable, ID: "n2", Source: []string{"doc:eval/rc-01/vendor1", "doc:eval/rc-01/vendor2"}, Table: &UITableSpec{
Columns: []UITableColumn{{Key: "k", Label: "维度"}, {Key: "v", Label: "值"}},
Rows: []UITableRow{{ID: "r1", Cells: [][2]string{{"k", "价格"}, {"v", "¥8/人/天"}}}},
}},
{Type: UISteps, ID: "n3", Steps: &UIStepsSpec{Current: 1, Steps: []UIStep{
{Label: "需求冻结", Status: "done"}, {Label: "契约定义", Status: "active"},
}}},
{Type: UIForm, ID: "n4", Fields: []UIField{
{Key: "years", Label: "归档年限", Type: "select", Options: []string{"3", "5"}, Required: true},
}},
},
}
}
func TestValidateSurfaceSpecValid(t *testing.T) {
if errs := ValidateSurfaceSpec(validSpec(), nil); len(errs) != 0 {
t.Fatalf("合法规格不应有违规: %v", errs)
}
// JSON 往返后仍合法(信封序列化面)。
b, err := json.Marshal(validSpec())
if err != nil {
t.Fatal(err)
}
var back SurfaceSpec
if err := json.Unmarshal(b, &back); err != nil {
t.Fatal(err)
}
if errs := ValidateSurfaceSpec(back, nil); len(errs) != 0 {
t.Fatalf("JSON 往返后不应有违规: %v", errs)
}
}
func TestValidateSurfaceSpecViolations(t *testing.T) {
cases := []struct {
name string
mut func(*SurfaceSpec)
want string
}{
{"未知类型", func(s *SurfaceSpec) { s.Nodes[0].Type = " hologram" }, "未知节点类型"},
{"重复节点 id", func(s *SurfaceSpec) { s.Nodes[1].ID = "n1" }, "重复节点 id"},
{"表格行列不齐", func(s *SurfaceSpec) {
s.Nodes[1].Table.Rows = []UITableRow{{Cells: [][2]string{{"k", "只有一列"}}}}
}, "不等长"},
{"select 缺 options", func(s *SurfaceSpec) {
s.Nodes[3].Fields[0].Options = nil
}, "缺 options"},
{"steps 越界", func(s *SurfaceSpec) { s.Nodes[2].Steps.Current = 5 }, "current 在范围内"},
{"空 title", func(s *SurfaceSpec) { s.Title = "" }, "title 必填"},
{"空 nodes", func(s *SurfaceSpec) { s.Nodes = nil }, "nodes 需非空"},
{"markdown 空", func(s *SurfaceSpec) {
s.Nodes[0].Children[0].Markdown = ""
}, "markdown 需要正文"},
{"progress 越界", func(s *SurfaceSpec) {
s.Nodes = append(s.Nodes, UINode{Type: UIProgress, Progress: &UIProgressSpec{Value: 1.5}})
}, "progress.value"},
{"未知来源", func(s *SurfaceSpec) {}, ""},
}
for _, c := range cases {
s := validSpec()
c.mut(&s)
errs := ValidateSurfaceSpec(s, nil)
if c.want == "" {
continue
}
found := false
for _, e := range errs {
if strings.Contains(e, c.want) {
found = true
break
}
}
if !found {
t.Errorf("%s: 期望包含 %q,得到 %v", c.name, c.want, errs)
}
}
// knownSources 非空时:未知来源拒绝、已知来源放行。
s := validSpec()
errs := ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true, "doc:eval/rc-01/vendor2": true})
if len(errs) != 0 {
t.Fatalf("已知来源全引用不应违规: %v", errs)
}
s.Nodes[0].Children[0].Source = append(s.Nodes[0].Children[0].Source, "doc:eval/rc-99/ghost")
errs = ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true})
if len(errs) == 0 {
t.Fatal("引用未知来源应拒绝")
}
}
func TestValidateSurfaceSpecLimits(t *testing.T) {
deep := UINode{Type: UISection, ID: "d0"}
cur := &deep
for i := 1; i <= SurfaceMaxDepth+1; i++ {
cur.Children = []UINode{{Type: UISection, ID: "d" + string(rune('0'+i%10))}}
cur = &cur.Children[0]
}
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: []UINode{deep}}, nil); len(errs) == 0 {
t.Fatal("超过嵌套深度应拒绝")
}
many := make([]UINode, SurfaceMaxNodes+1)
for i := range many {
many[i] = UINode{Type: UIMarkdown, Markdown: "x"}
}
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: many}, nil); len(errs) == 0 {
t.Fatal("超过节点数应拒绝")
}
}
func TestUINodeBackwardCompat(t *testing.T) {
// 旧卡片路径的 UINode JSON(无 id/source)解出后字段为零值,不受影响。
var n UINode
if err := json.Unmarshal([]byte(`{"type":"list","items":["a"]}`), &n); err != nil {
t.Fatal(err)
}
if n.ID != "" || n.Source != nil || n.Type != UIList || len(n.Items) != 1 {
t.Fatalf("旧负载解引用异常: %+v", n)
}
// 新字段双向序列化。
b, _ := json.Marshal(UINode{Type: UIMarkdown, ID: "m1", Source: []string{"s1"}, Markdown: "x"})
if !strings.Contains(string(b), `"id":"m1"`) || !strings.Contains(string(b), `"source":["s1"]`) {
t.Fatalf("新字段序列化缺失: %s", b)
}
}
+100
View File
@@ -0,0 +1,100 @@
package core
// TaskSummaryView 是 U1 写入 ActivityCard.Body 的权威语义草图。
// 完整 TaskSnapshot(findings[]、revision、streamScope)留待 U2;本类型只冻结
// 运行中/终态的精炼骨架字段与 Body KV 键名,供 Backend 生成、旧客户端回退阅读。
type TaskSummaryView struct {
Title string // 业务动宾短标题(通常已在 ActivityCard.Title)
Focus string // 正在做什么,≤2 行
FindingsNote string // U1:无成果时诚实占位;结构化 findings 属 U3
Remaining string
NextStep string
OutcomeLine string // 完成后的结论一行;运行中可空
}
// Body KV 键名(U1 冻结;用户默认卡身可见,勿改成内部材料键)。
const (
TaskSummaryKVStatus = "状态"
TaskSummaryKVFocus = "正在做"
TaskSummaryKVFindings = "当前发现"
TaskSummaryKVRemaining = "剩余"
TaskSummaryKVNextStep = "下一步"
)
// 状态行用户文案(与执行态映射;排队/进行中/完成/失败)。
const (
TaskSummaryStatusQueued = "排队中"
TaskSummaryStatusRunning = "进行中"
TaskSummaryStatusDone = "已完成"
TaskSummaryStatusFailed = "执行失败"
)
// 无阶段成果时的诚实占位(R03-a);不得编造业务发现。
const TaskSummaryNoFindingsYet = "尚无阶段结论,正在读取材料"
// RunningBody 生成运行中(含排队)进度卡 Body:Markdown 占位 + 固定骨架 KV。
// extraKV(材料键等)保留在同一 UIKV 节点,供系统去重,不充当用户进度正文。
func (v TaskSummaryView) RunningBody(statusLabel string, extraKV [][2]string) []UINode {
focus := stringsOr(v.Focus, "处理已登记工作")
findings := stringsOr(v.FindingsNote, TaskSummaryNoFindingsYet)
remaining := stringsOr(v.Remaining, "待任务进展后更新")
next := stringsOr(v.NextStep, "继续处理已登记工作")
if statusLabel == "" {
statusLabel = TaskSummaryStatusRunning
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, focus},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, remaining},
[2]string{TaskSummaryKVNextStep, next},
)
return []UINode{
{Type: UIMarkdown, Markdown: findings},
{Type: UIKV, KV: kv},
}
}
// FinishedBody 生成终态卡 Body:结论 Markdown + 骨架 KV(状态为已完成或执行失败)。
func (v TaskSummaryView) FinishedBody(statusLabel string, extraKV [][2]string) []UINode {
outcome := stringsOr(v.OutcomeLine, "(无结果摘要)")
findings := stringsOr(v.FindingsNote, outcome)
if statusLabel == "" {
statusLabel = TaskSummaryStatusDone
}
kv := append(append([][2]string(nil), extraKV...),
[2]string{TaskSummaryKVStatus, statusLabel},
[2]string{TaskSummaryKVFocus, stringsOr(v.Focus, "")},
[2]string{TaskSummaryKVFindings, findings},
[2]string{TaskSummaryKVRemaining, stringsOr(v.Remaining, "")},
[2]string{TaskSummaryKVNextStep, stringsOr(v.NextStep, "")},
)
// 去掉空值骨架行,避免完成卡出现空白「正在做」。
compact := make([][2]string, 0, len(kv))
for _, pair := range kv {
if pair[1] == "" && (pair[0] == TaskSummaryKVFocus || pair[0] == TaskSummaryKVRemaining || pair[0] == TaskSummaryKVNextStep) {
continue
}
compact = append(compact, pair)
}
return []UINode{
{Type: UIMarkdown, Markdown: outcome},
{Type: UIKV, KV: compact},
}
}
// CardDisplaysFailed 判断卡是否应按失败展示:含 CardFailed,以及旧客户端
// done + danger 兼容映射(U1 出口白名单)。
func CardDisplaysFailed(status CardStatus, tone string) bool {
if status == CardFailed {
return true
}
return status == CardDone && tone == "danger"
}
func stringsOr(s, fallback string) string {
if s == "" {
return fallback
}
return s
}
+90
View File
@@ -0,0 +1,90 @@
package core
import (
"encoding/json"
"strings"
"testing"
)
func TestTaskSummaryRunningBodyOmitsRawTaskAndUsesHonestPlaceholder(t *testing.T) {
v := TaskSummaryView{Focus: "核对需求与会议决定"}
body := v.RunningBody(TaskSummaryStatusQueued, [][2]string{{"材料键", "mk_x"}})
if len(body) != 2 || body[0].Type != UIMarkdown || body[1].Type != UIKV {
t.Fatalf("body shape = %+v", body)
}
if body[0].Markdown != TaskSummaryNoFindingsYet {
t.Fatalf("markdown = %q, want honest placeholder", body[0].Markdown)
}
rawTask := "逐页解析上传材料并对照验收条件" + strings.Repeat("x", 200)
if strings.Contains(body[0].Markdown, rawTask) {
t.Fatal("running body must not embed raw task text")
}
got := map[string]string{}
for _, kv := range body[1].KV {
got[kv[0]] = kv[1]
}
for _, key := range []string{TaskSummaryKVStatus, TaskSummaryKVFocus, TaskSummaryKVFindings, TaskSummaryKVRemaining, TaskSummaryKVNextStep, "材料键"} {
if _, ok := got[key]; !ok {
t.Fatalf("missing kv %q in %v", key, got)
}
}
if got[TaskSummaryKVStatus] != TaskSummaryStatusQueued {
t.Fatalf("status = %q", got[TaskSummaryKVStatus])
}
if got[TaskSummaryKVFindings] != TaskSummaryNoFindingsYet {
t.Fatalf("findings = %q", got[TaskSummaryKVFindings])
}
}
func TestTaskSummaryFinishedBodyFailureLabel(t *testing.T) {
v := TaskSummaryView{OutcomeLine: "执行失败:boom"}
body := v.FinishedBody(TaskSummaryStatusFailed, nil)
if body[0].Markdown != "执行失败:boom" {
t.Fatalf("outcome = %q", body[0].Markdown)
}
found := false
for _, kv := range body[1].KV {
if kv[0] == TaskSummaryKVStatus {
found = true
if kv[1] != TaskSummaryStatusFailed {
t.Fatalf("status = %q, want %q", kv[1], TaskSummaryStatusFailed)
}
}
}
if !found {
t.Fatal("missing status kv")
}
}
func TestTaskSummaryViewJSONRoundTrip(t *testing.T) {
in := TaskSummaryView{
Title: "核对三份材料", Focus: "核对需求", FindingsNote: TaskSummaryNoFindingsYet,
Remaining: "评审意见", NextStep: "整理冲突", OutcomeLine: "",
}
b, err := json.Marshal(in)
if err != nil {
t.Fatal(err)
}
var out TaskSummaryView
if err := json.Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
if out != in {
t.Fatalf("round-trip = %+v, want %+v", out, in)
}
}
func TestCardDisplaysFailed(t *testing.T) {
if !CardDisplaysFailed(CardFailed, "danger") {
t.Fatal("CardFailed should display failed")
}
if !CardDisplaysFailed(CardDone, "danger") {
t.Fatal("legacy done+danger should display failed")
}
if CardDisplaysFailed(CardDone, "ok") {
t.Fatal("done+ok must not display failed")
}
if CardDisplaysFailed(CardActive, "info") {
t.Fatal("active must not display failed")
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"id": "demo.brief",
"title": "简报",
"description": "按主题写一份简报",
"intentId": "demo.brief",
"maxRetries": 6,
"steps": [
{
"id": "length",
"title": "篇幅",
"kind": "hitl",
"hitl": {
"question": "篇幅?",
"kind": "choice",
"choices": [
{
"id": "short",
"label": "短",
"description": "一页",
"impact": "更快"
}
],
"allowFreeInput": false
}
},
{
"id": "draft",
"title": "起草",
"kind": "prompt",
"prompt": "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
"agent": "research",
"critic": {
"criteria": "必须点明主题",
"maxRetries": 1
}
},
{
"id": "lookup",
"title": "查时间",
"kind": "tool",
"tool": "time_now",
"args": {
"tz": "UTC"
}
},
{
"id": "save",
"title": "存稿",
"kind": "artifact",
"prompt": "{{steps.draft.output}}",
"artifact": "brief.md"
},
{
"id": "note",
"title": "通知",
"kind": "card",
"card": {
"kind": "result",
"title": "简报已写好"
}
},
{
"id": "run",
"title": "脚本",
"kind": "script",
"script": "scripts/demo.py"
}
]
}
+15 -11
View File
@@ -23,16 +23,20 @@ type ToolDesc struct {
// Hidden 预留能力形态:默认不对模型与 /api/tools 列表可见,须运行时开关
// (如 runtime.fsTools)或管理动作开启后可用(提案 2026-09-30-file-preview-and-reserved-capabilities)。
Hidden bool `json:"hidden,omitempty"`
// ReadOnly、Edits、Risk 是工具注册时的权限声明。Risk 取 low、medium、high。
ReadOnly bool `json:"readOnly,omitempty"`
Edits bool `json:"edits,omitempty"`
Risk string `json:"risk,omitempty"`
}
// 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"`
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"`
}
@@ -44,9 +48,9 @@ type ToolCallRequest struct {
}
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"` // 超长转工件
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"` // 超长转工件
}
+109 -5
View File
@@ -15,6 +15,8 @@ export interface Conversation {
id: ID; projectId: ID; owner?: ID; title: string; profileId?: ID;
pinned?: boolean; archived?: boolean;
allowCrossRead?: boolean; crossPrivate?: boolean;
permissionMode?: 'default' | 'plan' | 'auto_edit' | '';
sessionNote?: string;
createdAt: string; updatedAt: string;
}
@@ -22,7 +24,7 @@ export type Role = 'user' | 'assistant' | 'system';
export type PartType =
| 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref'
| 'hitl' | 'plan' | 'error' | 'card';
| 'hitl' | 'plan' | 'error' | 'card' | 'check';
export interface Part {
type: PartType;
@@ -34,6 +36,7 @@ export interface Part {
plan?: PlanSnapshot;
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
error?: { code: string; message: string };
check?: { passed: boolean; reason?: string }; // 终轮自动检查,过程区一行
process?: boolean; // run 终态后折叠为摘要行
durationMs?: number; // thinking 部件的思考时长(毫秒);工具耗时在 toolCall.durationMs
}
@@ -41,17 +44,27 @@ export interface Part {
export interface Message {
id: ID; conversationId: ID; runId?: ID; role: Role;
parts: Part[]; createdAt: string;
excluded?: boolean;
variantGroup?: string;
variantIndex?: number;
}
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 type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'done';
export interface Usage {
promptTokens: number; completionTokens: number; totalTokens: number;
durationMs: number; toolCalls: number;
model?: string;
// N3 计费拆分(additive;旧记录缺省为 0):
// Last* 是最近一次请求的用量,Cache* 是缓存命中累计。
lastPromptTokens?: number;
lastCompletionTokens?: number;
cacheReadTokens?: number;
cacheWriteTokens?: number;
}
export interface Run {
@@ -79,12 +92,14 @@ export interface TodoList {
// ---- 流式事件 ----
export type StreamEventType =
| 'message.delta' | 'thinking.delta' | 'tool.call' | 'tool.result'
| 'run.phase' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
| 'message.delta' | 'thinking.delta'
| 'tool.call.delta' | 'tool.call' | 'tool.output' | 'tool.result'
| 'run.phase' | 'run.check' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
| 'agent.spawn' | 'agent.progress' | 'agent.result'
| 'artifact.created' | 'artifact.updated'
| 'run.completed' | 'run.failed' | 'error'
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved';
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved'
| 'surface.updated';
export interface StreamEvent<P = unknown> {
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
@@ -93,6 +108,9 @@ export interface StreamEvent<P = unknown> {
// payload 形状(按 type 判别;未知字段忽略)
export interface MessageDeltaPayload { index: number; text: string }
export interface ToolCallDeltaPayload { index: number; callId?: string; name?: string; delta: string }
export interface ToolOutputPayload { callId: string; stream: 'stdout' | 'stderr'; text: string }
export interface ToolResultPayload { callId: string; status: string; summary?: string; truncated?: boolean; durationMs?: number }
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 }
@@ -102,11 +120,19 @@ export interface RunCompletedPayload {
context?: ContextObservation
}
interface ContextSection {
name: string
estTokens: number
}
export interface ContextObservation {
items: string[]
truncations?: string[]
estChars: number
sections?: ContextSection[]
prefixHash?: string
}
export interface RunCheckPayload { passed: boolean; reason?: string }
export interface RunFailedPayload { code: string; message: string }
// ---- 收件箱卡片 ----
@@ -144,6 +170,7 @@ export interface HITLCard {
choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string;
timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean;
anchorMessageId?: ID; blocked?: boolean; blockReason?: string;
detail?: string;
}
export interface HITLResponse {
@@ -158,6 +185,9 @@ export type UINodeType =
export interface UINode {
type: UINodeType;
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
id?: string;
source?: string[];
title?: string;
items?: string[];
kv?: [string, string][];
@@ -239,6 +269,8 @@ export interface SettingDesc {
default?: unknown; options?: string[];
effect: 'hot' | 'new-run' | 'restart';
secret?: boolean;
/** Setting ownership; an omitted scope retains legacy personal compatibility. */
scope?: 'system' | 'user' | 'project' | 'project-personal';
}
// ---- 插件 ----
@@ -261,6 +293,36 @@ export interface PluginPanel {
id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string;
}
/** 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。 */
export interface WorkflowDef {
id: string; title: string; description?: string; intentId?: string;
/** 整条工作流的 critic 重试上限。 */
maxRetries?: number;
steps: WorkflowStep[];
}
export interface StepCritic {
criteria: string;
/** 本步重试上限;缺省 1 次。 */
maxRetries?: number;
}
export interface WorkflowStep {
id: string; title: string;
kind: 'prompt' | 'tool' | 'hitl' | 'artifact' | 'card' | 'script';
prompt?: string;
/** prompt 步骤的子代理 profile,缺省 general。 */
agent?: string;
tool?: string;
args?: unknown;
hitl?: { question: string; kind: HITLKind; choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string };
artifact?: string;
card?: { kind: CardKind; title: string; tone?: string; body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[] };
/** script 步骤在插件目录内的相对路径。 */
script?: string;
critic?: StepCritic;
}
export interface PluginWorkspace {
id: string; title: string; icon?: string; shortcut?: string;
badgeTool?: string; body: UINode[]; refreshTool?: string;
@@ -295,3 +357,45 @@ export interface UploadInfo {
artifactId: ID; filename: string; mime?: string; size: number;
parsed?: ParsedDocument;
}
// ---- Generative UI Surface(提案 U2/U4;与 Go surface.go 同步)----
export interface SurfaceCompat {
schemaVersion: number;
catalogVersion: string;
pluginVersion?: string;
}
export interface SurfaceIdentity {
surfaceId: ID;
owner: string;
projectId?: ID;
conversationId?: ID;
sourceRunId?: ID;
pluginId: string;
}
export interface SurfaceSpec {
title: string;
nodes: UINode[];
}
export interface SurfaceSnapshot {
identity: SurfaceIdentity;
compat: SurfaceCompat;
revision: number;
baseRevision?: number;
contextVersion?: string;
activationEpoch: number;
spec: SurfaceSpec;
semanticText?: string;
}
export interface SurfaceMeta {
surfaceId: ID;
title: string;
pluginId: string;
revision: number;
conversationId?: ID;
updatedAt: string;
}
+6 -1
View File
@@ -28,7 +28,12 @@ const (
)
type UINode struct {
Type UINodeType `json:"type"`
Type UINodeType `json:"type"`
// ID / Source 是 Generative UI Surface 层的节点级标注(提案 U2):ID 整页
// 唯一且稳定(交互与修订对位);Source 为支撑该节点事实内容的来源标识,
// 授权校验在数据绑定层(U3)。均为可选:旧卡片/表单路径不填不受影响。
ID string `json:"id,omitempty"`
Source []string `json:"source,omitempty"`
Title string `json:"title,omitempty"`
Items []string `json:"items,omitempty"` // list
KV [][2]string `json:"kv,omitempty"` // kv