Why: roles had no way to carry operational know-how into executions (RoleSpec.Skills never reached the backend), and the system prompt was assembled ad hoc with no environment facts or skill index. Evidence: go vet ./... clean; go test ./... 12 packages ok. Skills cases cover directory-authoritative naming, optional frontmatter description extraction, invalid dir skipping, root priority on name clash, missing-root tolerance, fail-closed unknown-skill loads, 64 KiB body / 200-byte description caps, load_skill registration only when skills exist, and end-to-end adapter wiring (index in system prompt, declared-skill validation failing closed on ghost skills, workspace .agent/skills auto-merge). promptx cases pin section order, sorted index, reasoning-profile sentence, and env defaults. Impact: new runtime/skills + runtime/promptx packages; native.Options gains SkillsRoots; adapters/native system prompt now composes instructions + environment (date/model/workspace/profile) + declared skills + index + deliverables; declared-but-missing skills fail the step (configuration error, not silent). Refs: plan B6; RoleSpec.Skills plumbing lands with the squad batch.
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
// Package promptx 组装执行内核的系统提示。
|
|
//
|
|
// 职责边界(要求与实现分离):人格/角色内容由上层(产品提示词库、角色
|
|
// Instructions)提供并经 Parts.Instructions 传入;本包只做结构化装配——
|
|
// 环境事实、技能索引、交付物与档位约束、护栏段。工具清单仍由 runtime
|
|
// 循环在系统提示之后追加,不在本包。
|
|
package promptx
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// EnvFacts 是注入系统提示的环境事实(由适配层填写,测试可固定)。
|
|
type EnvFacts struct {
|
|
Date string // 如 2026-10-01(周一)
|
|
Model string
|
|
WorkspaceRoot string
|
|
Profile string // workspace | reasoning
|
|
}
|
|
|
|
// IndexEntry 是技能索引行。
|
|
type IndexEntry struct {
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
// Parts 是一次执行的装配输入。
|
|
type Parts struct {
|
|
Instructions string
|
|
// SkillsDeclared 是本次执行显式要求的技能名(角色声明)。
|
|
SkillsDeclared []string
|
|
// SkillsIndex 是可用技能索引(渐进披露:只列名与描述)。
|
|
SkillsIndex []IndexEntry
|
|
Deliverables []string
|
|
Env EnvFacts
|
|
}
|
|
|
|
// Compose 产出系统提示正文。
|
|
func Compose(p Parts) string {
|
|
var b strings.Builder
|
|
if strings.TrimSpace(p.Instructions) != "" {
|
|
b.WriteString(strings.TrimSpace(p.Instructions))
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
// 环境事实块。
|
|
env := p.Env
|
|
if env.Date == "" && env.Model == "" && env.WorkspaceRoot == "" {
|
|
env.Date = time.Now().Format("2006-01-02")
|
|
}
|
|
var envLines []string
|
|
if env.Date != "" {
|
|
envLines = append(envLines, "date: "+env.Date)
|
|
}
|
|
if env.Model != "" {
|
|
envLines = append(envLines, "model: "+env.Model)
|
|
}
|
|
if env.WorkspaceRoot != "" {
|
|
envLines = append(envLines, "workspace: "+env.WorkspaceRoot)
|
|
}
|
|
if env.Profile != "" {
|
|
envLines = append(envLines, "tool profile: "+env.Profile)
|
|
}
|
|
if len(envLines) > 0 {
|
|
b.WriteString("## Environment\n")
|
|
b.WriteString(strings.Join(envLines, "\n"))
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
// 技能块:声明优先 + 全量索引 + 按需加载指引。
|
|
if len(p.SkillsDeclared) > 0 || len(p.SkillsIndex) > 0 {
|
|
b.WriteString("## Skills\n")
|
|
if len(p.SkillsDeclared) > 0 {
|
|
b.WriteString("Declared for this task (load with load_skill before relying on them): ")
|
|
b.WriteString(strings.Join(p.SkillsDeclared, ", "))
|
|
b.WriteString("\n")
|
|
}
|
|
if len(p.SkillsIndex) > 0 {
|
|
entries := make([]IndexEntry, len(p.SkillsIndex))
|
|
copy(entries, p.SkillsIndex)
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
|
|
b.WriteString("Available skills (name — description; use load_skill for full instructions):\n")
|
|
for _, e := range entries {
|
|
line := "- " + e.Name
|
|
if e.Description != "" {
|
|
line += " — " + e.Description
|
|
}
|
|
b.WriteString(line + "\n")
|
|
}
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// 交付物与档位约束(保持既有语义原文,行为兼容)。
|
|
b.WriteString("Deliverable files: ")
|
|
b.WriteString(strings.Join(p.Deliverables, ", "))
|
|
b.WriteString(".")
|
|
if p.Env.Profile == "reasoning" {
|
|
b.WriteString(" Reply with the deliverable text only. Tools are disabled.")
|
|
} else {
|
|
b.WriteString(" Use workspace tools to write those files. Do not invent paths outside the workspace.")
|
|
}
|
|
return strings.TrimSpace(b.String())
|
|
}
|