Compare commits

..
Author SHA1 Message Date
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
12 changed files with 695 additions and 96 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)——步骤执行时生成
+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)
}
}
+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"` // 超长转工件
}
+60 -4
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,8 +92,9 @@ 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'
@@ -93,6 +107,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 +119,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 +169,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 {
@@ -261,6 +287,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;
+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