Add the generative-ui plugin, capability gating, and surface projection
ci / vet-and-test (pull_request) Successful in 42s
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:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user