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
74 lines
2.3 KiB
Go
74 lines
2.3 KiB
Go
// ExecuteTool 工件回填契约(交付物卡片根因修复的验收):注册表工具经
|
|
// rc.WriteArtifact 写入的工件必须出现在 ToolCallResult.Artifact 上——
|
|
// llm.go 据此生成对话内 artifact_ref 卡片部件。此前只有超长截断路径会设置它。
|
|
package runmgr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"agent-system-backend/internal/bus"
|
|
"agent-system-backend/internal/settings"
|
|
"agent-system-backend/internal/store"
|
|
"agent-system-backend/internal/tools"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func newExecEnv(t *testing.T) (*runCtx, *store.Store) {
|
|
t.Helper()
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "exec.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
b := bus.New()
|
|
reg := tools.NewRegistry()
|
|
if err := reg.RegisterBuiltin(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proj, err := st.CreateProject("alice", "p1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := st.CreateConversation("alice", proj.ID, "c1", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := settings.NewService(st)
|
|
m := &Manager{st: st, bus: b, registry: reg, settings: svc, events: newEventBuffer(st, b)}
|
|
eff := svc.Effective("alice", "")
|
|
c := &runCtx{
|
|
mgr: m, run: core.Run{ProjectID: proj.ID, ConversationID: conv.ID, Owner: "alice"},
|
|
prof: core.AgentProfile{}, eff: eff, ctx: context.Background(),
|
|
}
|
|
return c, st
|
|
}
|
|
|
|
func TestExecuteToolBackfillsArtifact(t *testing.T) {
|
|
c, _ := newExecEnv(t)
|
|
res := c.ExecuteTool(core.ToolCallRequest{
|
|
CallID: "call1", Name: "write_artifact",
|
|
Arguments: json.RawMessage(`{"name":"summary.md","kind":"document","content":"# 摘要正文"}`),
|
|
})
|
|
if res.Status != "ok" {
|
|
t.Fatalf("write_artifact: %s", res.Output)
|
|
}
|
|
if res.Artifact == nil {
|
|
t.Fatal("ToolCallResult.Artifact 未回填——对话内交付物卡片不会出现(回归)")
|
|
}
|
|
if res.Artifact.Name != "summary.md" {
|
|
t.Errorf("回填工件名不符: %+v", res.Artifact)
|
|
}
|
|
}
|
|
|
|
// 空手工具(如 time_now)不得凭空挂 Artifact。
|
|
func TestExecuteToolNoArtifactByDefault(t *testing.T) {
|
|
c, _ := newExecEnv(t)
|
|
res := c.ExecuteTool(core.ToolCallRequest{CallID: "c1", Name: "time_now", Arguments: json.RawMessage(`{}`)})
|
|
if res.Status != "ok" || res.Artifact != nil {
|
|
t.Errorf("无产物工具不应携带 Artifact: %+v", res)
|
|
}
|
|
}
|