Add the generative-ui plugin, capability gating, and surface projection
ci / vet-and-test (pull_request) Successful in 42s

Why: Generative UI U3 - the agent needs a real submission path and the
next turn needs to see prior pages: a built-in in-process plugin
registers generative-ui.present (exposeAsTool) carrying the catalog
(prompt=validation single source) with a schema-repair strategy
measured against real MiniMax-M3 deviations; the command bus gains an
owner-scoped tool gate so a disabled plugin stops injecting its tool
(execution guard remains in surfacesvc); ctxasm projects saved surfaces
back into context so the model can revise existing structure; doc-refs
are authorized against the owner's real artifacts.

Evidence: unit suites green (genui guards/epoch/doc-ref, gate fail-closed,
projection content); full go test ./... green; cmdbus/ctxasm also green
under AGENT_PG_TEST_DSN; real-model closed loop (env-gated
TestRealModelSurfaceLoop) passes: task->spec->save->projection->revise
with revision 1->2 and structure change, CNY 0.08 total.

Impact: New internal/genui package; additive cmdbus/ctxasm/runmgr/
runtime hooks; no storage or contract changes (U2 layer untouched).

Refs: Documents proposal r2 U3; Core PR #25/#26; Backend #143; Dev task
feature/generative-ui-u3
This commit is contained in:
2026-10-11 10:38:15 +00:00
parent 2bfebe378c
commit 367dc207b3
15 changed files with 939 additions and 3 deletions
+53
View File
@@ -41,6 +41,9 @@ type AssembleOptions struct {
// ThinkReplay 为真时,历史 assistant 消息的思考部件以 <think> 块原样回传
//(交错思考模型如 MiniMax 的官方要求:丢弃思考内容会降低后续轮性能)。
ThinkReplay bool
// Surfaces 是当前会话最近保存的生成页面(U3/R6):以语义投影并入上下文,
// 使下一轮模型能解释并修改已有结构(修改走 generative-ui.present 重提)。
Surfaces []core.SurfaceSnapshot
}
// AssembleInput 组装模型输入消息。sys 为提示词库组装出的 system 段(可为多段;
@@ -58,6 +61,17 @@ func AssembleInput(prof core.AgentProfile, eff core.EffectiveSettings, history [
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleSystem, Content: prof.SystemPrompt})
rep.Items = append(rep.Items, "system(画像自定义段)")
}
if len(opts.Surfaces) > 0 {
var b strings.Builder
b.WriteString("[本会话已保存的生成页面(结构摘要;正文以预览区为准)]\n")
for _, snap := range opts.Surfaces {
b.WriteString(projectSurface(snap))
b.WriteString("\n")
}
b.WriteString("用户要求调整页面时,按结构摘要理解现状并用 generative-ui.present 提交修订(带 surfaceId 与 baseRevision)。")
msgs = append(msgs, agentllm.Message{Role: agentllm.RoleSystem, Content: b.String()})
rep.Items = append(rep.Items, fmt.Sprintf("surface(%d)", len(opts.Surfaces)))
}
maxHist := prof.Context.MaxHistoryMessages
if maxHist <= 0 {
@@ -499,3 +513,42 @@ func InjectMemory(sys []agentllm.Message, entries []core.MemoryEntry) []agentllm
out = append(out, agentllm.Message{Role: agentllm.RoleSystem, Content: b.String()})
return out
}
// projectSurface 是 ctxasm 侧的结构摘要投影(与 surfacesvc.ProjectSurface 同义;
// 独立实现避免 ctxasm→surfacesvc 依赖边)。
func projectSurface(snap core.SurfaceSnapshot) string {
var b strings.Builder
fmt.Fprintf(&b, "- 页面 %s(修订 %d)%q", string(snap.Identity.SurfaceID), snap.Revision, snap.Spec.Title)
if snap.SemanticText != "" {
fmt.Fprintf(&b, ":%s", snap.SemanticText)
}
b.WriteString("\n 结构:")
var walk func(n core.UINode)
first := true
walk = func(n core.UINode) {
label := string(n.Type)
if n.Title != "" {
label += "(\"" + n.Title + "\")"
}
if n.ID != "" {
label += "#" + n.ID
}
if len(n.Source) > 0 {
label += "[" + strings.Join(n.Source, ",") + "]"
}
if first {
b.WriteString(label)
first = false
} else {
b.WriteString(" > " + label)
}
for _, c := range n.Children {
walk(c)
}
}
for _, n := range snap.Spec.Nodes {
walk(n)
}
b.WriteString("\n 修订此页:present 带 surfaceId=" + string(snap.Identity.SurfaceID) + " 与 baseRevision=" + fmt.Sprint(snap.Revision) + "。")
return b.String()
}
+52
View File
@@ -0,0 +1,52 @@
package ctxasm
// U3/R6:已保存页面的语义投影并入上下文——下一轮模型能看到结构、来源与修订
// 入口(present + baseRevision),可据此解释并修改已有 table/compare/tabs。
import (
"strings"
"testing"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
func surfaceSnap() core.SurfaceSnapshot {
return core.SurfaceSnapshot{
Identity: core.SurfaceIdentity{SurfaceID: "surf_x", Owner: "alice", PluginID: "generative-ui", ConversationID: "c1"},
Revision: 2,
Spec: core.SurfaceSpec{Title: "选型对比", Nodes: []core.UINode{
{Type: core.UISection, ID: "n1", Title: "概览", Children: []core.UINode{
{Type: core.UIMarkdown, ID: "n1a", Source: []string{"doc:a"}, Markdown: "结论"},
}},
{Type: core.UITable, ID: "n2", Source: []string{"doc:a", "doc:b"}},
}},
SemanticText: "三工具对比页",
}
}
func TestSurfaceProjectionInContext(t *testing.T) {
msgs, rep := AssembleInput(core.AgentProfile{}, nil, nil,
core.Message{Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "改一下"}}},
nil, nil, nil, nil, nil, nil, AssembleOptions{Surfaces: []core.SurfaceSnapshot{surfaceSnap()}})
joined := make([]string, len(msgs))
for i, m := range msgs {
joined[i] = m.Content
}
all := strings.Join(joined, "\n---\n")
for _, need := range []string{"surf_x", "修订 2", "选型对比", "section(\"概览\")#n1", "markdown#n1a[doc:a]", "table#n2", "baseRevision=2"} {
if !strings.Contains(all, need) {
t.Fatalf("projection missing %q in assembled context:\n%s", need, all)
}
}
found := false
for _, it := range rep.Items {
if strings.HasPrefix(it, "surface(") {
found = true
}
}
if !found {
t.Fatalf("report should count surface injection: %v", rep.Items)
}
_ = agentllm.RoleSystem
}