mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
Upgrade the parallel tool planner from SideEffectRead probes to the PR5 CapabilitiesOf contract: EffectReadOnly + ThreadSafe + auto-allowed, with resource-key conflict boundaries so same-path reads stay sequential. Mark audited pure reads ThreadSafe (read_file, read_minified_file, list_directory, glob, grep, skill, web_fetch). Glob/grep use scopedScan directory keys (nil for workspace-wide "." so scans do not false-conflict). Tests cover the capability gate, key conflicts, extendParallelRun windows, and catalog ThreadSafe audit for concurrent-safe reads. Co-authored-by: KRATOS <[email protected]>
82 lines
2.8 KiB
Go
82 lines
2.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/skills"
|
|
)
|
|
|
|
// skillTool lets the model pull a reusable instruction "skill" into context on
|
|
// demand (PRD F15). It reads the skills directory itself via the internal/skills
|
|
// loader and returns the named skill's markdown body as its Output, so the model
|
|
// can opt into reusable guidance only when relevant. It is read-only.
|
|
type skillTool struct {
|
|
baseTool
|
|
dir string
|
|
}
|
|
|
|
// NewSkillTool builds the skill tool. An empty dir resolves to the standard
|
|
// skills data directory (skills.DefaultDir); pass an explicit dir in tests.
|
|
func NewSkillTool(dir string) *skillTool {
|
|
if strings.TrimSpace(dir) == "" {
|
|
dir = skills.DefaultDir(nil)
|
|
}
|
|
return &skillTool{
|
|
dir: dir,
|
|
baseTool: baseTool{
|
|
name: "skill",
|
|
description: "Load a named Zero skill and return its instructions as the tool output. " +
|
|
"Skills are reusable, on-demand instruction sets (e.g. project conventions or confirmation policies). " +
|
|
"Call this when a relevant skill exists; an unknown name returns the list of available skills.",
|
|
parameters: Schema{
|
|
Type: "object",
|
|
Properties: map[string]PropertySchema{
|
|
"name": {
|
|
Type: "string",
|
|
Description: "The name of the skill to load.",
|
|
},
|
|
"skill": {
|
|
Type: "string",
|
|
Description: "Alias for name; supply either name or skill.",
|
|
},
|
|
},
|
|
// Intentionally no strict Required: the tool needs exactly one of
|
|
// name/skill, which Run enforces via aliasedStringArg. Declaring both
|
|
// here keeps the alias usable under schema validators that reject
|
|
// unknown keys (AdditionalProperties:false).
|
|
AdditionalProperties: false,
|
|
},
|
|
safety: readOnlySafety("Reads a local skill file; gathers reusable instructions only."),
|
|
capabilities: ToolCapabilities{Effect: EffectReadOnly, ThreadSafe: true},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run loads the named skill and returns its Content. Unknown names return a
|
|
// clear error listing the available skill names so the model can self-correct.
|
|
func (tool *skillTool) Run(_ context.Context, args map[string]any) Result {
|
|
name, err := aliasedStringArg(args, []string{"name", "skill"}, "", true, false)
|
|
if err != nil {
|
|
return errorResult("Error: Invalid arguments for skill: " + err.Error())
|
|
}
|
|
|
|
loaded, err := skills.Load(tool.dir)
|
|
if err != nil {
|
|
return errorResult("Error: failed to load skills: " + err.Error())
|
|
}
|
|
if len(loaded) == 0 {
|
|
return errorResult(fmt.Sprintf("Error: no skills are available (looked in %s).", tool.dir))
|
|
}
|
|
|
|
names := make([]string, 0, len(loaded))
|
|
for _, skill := range loaded {
|
|
if skill.Name == name {
|
|
return okResult(skill.Content)
|
|
}
|
|
names = append(names, skill.Name)
|
|
}
|
|
return errorResult(fmt.Sprintf("Error: unknown skill %q. Available skills: %s.", name, strings.Join(names, ", ")))
|
|
}
|