Files
weichen d6bef8663f Return deliverable generation to the agent and fix the artifact-card root cause
Why: two field reports against the live build — deliverable cards never
appeared in chat (only model-written filenames) and the artifacts panel
felt dead. Root cause found in runmgr.ExecuteTool: registry tools that
write artifacts via rc.WriteArtifact never had res.Artifact set (only
the oversized-result path did), so llm.go never emitted artifact_ref
parts for real tool output; the unit suite passed via fakes that set
it directly. Architecturally, document generation moves back to the
agent (user directive): system provides execution primitives and the
delivery bridge, not domain tools like write_docx.

Evidence:
- runmgr: toolCtx tracks artifacts written during a call and
  ExecuteTool backfills res.Artifact (new runmgr test proves
  write_artifact now surfaces one; negative test: no phantom artifact)
- new tools/register_artifact: bridge from workspace files (binary
  docx/xlsx/pptx/pdf/images, <=10MB) to deliverable artifacts;
  run_shell/run_python interpreter executors are designed
  (script-to-disk + whitelisted bash/python3, sandbox cwd, caps) and
  land in a follow-up batch behind the security gate decision
- removed builtin write_docx + its prompt; write_artifact prompt now
  text-only with explicit no-binary guidance; new always-on base
  prompt base.deliverables teaches the generate->register->summarize
  workflow with python-docx/openpyxl/python-pptx recipes;
  task.doc-minutes rewritten to the workflow
- final-output contract: inline <think> blocks in streamed content are
  split into thinking (process) parts so the final reply stays clean
  (unit + runner tests)
- go test ./... 19 packages ok; vet clean

Impact: every tool artifact now lands as an in-chat deliverable card;
mock #doc unchanged as demo fixture; binary office mimes stay out of
the text index; write_docx callers see the tool gone (prompt reroutes
them).

Refs: task feature/agent-native-deliverables-r1
2026-10-01 03:32:30 +00:00

69 lines
2.3 KiB
Go

// 最终输出契约测试:内联 <think> 折离 + 终轮正文干净(llm.go splitThinkBlock)。
package runtime
import (
"context"
"strings"
"testing"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
// TestSplitThinkBlock 单测:首部 <think>…</think> 剥离;未闭合/非首部/空块按原文。
func TestSplitThinkBlock(t *testing.T) {
cases := []struct {
in string
body, think string
}{
{"<think>推理</think>答案", "答案", "推理"},
{" <think>推理</think>\n答案", "答案", "推理"},
{"<think>推理</think>", "", "推理"},
{"<think>未闭合", "<think>未闭合", ""},
{"正文 <think>混入</think>", "正文 <think>混入</think>", ""},
{"没有标签的正文", "没有标签的正文", ""},
{"<think></think>答案", "答案", ""},
}
for _, c := range cases {
b, th := splitThinkBlock(c.in)
if b != c.body || th != c.think {
t.Errorf("splitThinkBlock(%q) = (%q, %q), want (%q, %q)", c.in, b, th, c.body, c.think)
}
}
}
// TestLLMRunnerThinkStrippedFromReply 契约:模型把推理以 <think> 内联进最终回复时,
// 落库拆为「思考(过程)+ 干净正文」——最终输出即对用户的回复。
func TestLLMRunnerThinkStrippedFromReply(t *testing.T) {
turns := [][]agentllm.Event{
{
agentllm.Event{Kind: agentllm.KindTextDelta, Text: "<think>用户要纪要,我分三步……</think>\n已生成会议纪要,要点三项。"},
agentllm.Event{Kind: agentllm.KindMessageEnd},
},
}
r := &LLMRunner{Stream: scriptStream(turns, nil)}
rc := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "m"}}}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
var think, text *core.Part
for i := range rc.assistant {
p := rc.assistant[i]
if p.Type == core.PartThinking {
think = &rc.assistant[i]
}
if p.Type == core.PartText {
text = &rc.assistant[i]
}
}
if think == nil || !strings.Contains(think.Text, "分三步") || !think.Process {
t.Errorf("内联推理未折离为思考部件: %+v", rc.assistant)
}
if text == nil || strings.Contains(text.Text, "<think>") || !strings.Contains(text.Text, "要点三项") {
t.Errorf("最终正文不干净: %+v", text)
}
if text.Process {
t.Errorf("终轮正文不得标记 Process: %+v", text)
}
}