mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* polish: cut per-turn overhead, stabilize cache, add /new Context economy - Compaction: preserveLast 8->6, triggerRatio 0.8->0.7 (both copies in sync). - System prompt: kill the "ELABORATE summary" directive; scale summary to the work. Comment discipline moved into the core prompt for all model families (was Anthropic-only); Minimax/DeepSeek/Qwen/etc. now get it. - Token-budget regression test ratchets the fixed per-turn overhead (base prompt + eager tool schemas) using the runtime's own estimator, so silent prompt/schema growth fails CI. Response quality - defaultResponseStyle balanced->concise. Reversible via /style. Tool discovery - <available_skills> block in the system prompt lists installed skills (name + one-line description) so the model loads the right one on the first call instead of guessing and reading the failure. Budget-capped at 640 bytes with an "...and N more" fallback; renders nothing when no skills are installed (byte-identical to prior prompt). Session lifecycle - /new starts a fresh session in place; previous session stays on disk and is named in the confirmation for /resume. Guards against mid-run reset. - /clear now notes that context persists and points to /new. Performance & caching - partitionTools no longer alpha-sorts the whole tools array every turn, which inserted a loaded deferred tool mid-array and shifted the cached prefix on every load. It now builds append-only regions [eager sorted] + [tool_search] + [loaded deferred sorted] so a mid-session load grows the tail instead of busting the eager prefix. Regression test asserts the eager block precedes any loaded tool. - /context now shows cache hit rate (flags low rate over >5 turns as a possible unstable prefix) and rolling average turn latency with time-to-first-token. Both reset on /new. Measured baselines (auto-mode, no workspace): base prompt 3162 tokens, eager tool schemas 2433 across 12 tools. Budget ceilings set ~10% above. Deliberately deferred (need a decision): inverted safety-policy rewrite (sandbox doesn't catch ~half the B-tier rules), core-tool/web_fetch deferral (activation-semantics blocker), aggressive prompt slim. * fix(tui): clear staged attachments + queued message on /new CodeRabbit: startNewSession reset conversation/usage/latency state but not the staged input (pendingImages, pendingImageLabels, pendingDocuments, queuedMessage), which is only consumed at prompt-submit. Without clearing it, the fresh session's first prompt silently inherited the previous session's images/documents/queued text. Test now stages all four and asserts they clear.
108 lines
4.2 KiB
Go
108 lines
4.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/Gitlawb/zero/internal/agent"
|
|
"github.com/Gitlawb/zero/internal/hooks"
|
|
"github.com/Gitlawb/zero/internal/plugins"
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
// pluginActivation holds what plugin activation contributed to a bootstrap so the
|
|
// later dispatcher + skill wiring can consume it: the plugin hook definitions and
|
|
// the plugin skill search roots. The zero value (no plugins) is inert — the
|
|
// dispatcher gets no extra hooks and the skill tool keeps only the default dir.
|
|
type pluginActivation struct {
|
|
hooks []hooks.Definition
|
|
skillRoots []string
|
|
}
|
|
|
|
// activatePlugins loads the workspace's plugins and makes their declared
|
|
// extensions live: plugin tools are registered into registry here, and the
|
|
// returned activation carries the plugin hooks + skill roots for the caller to
|
|
// fold into the hook dispatcher and the skill tool.
|
|
//
|
|
// It fails OPEN: any load error (or a malformed plugin) is surfaced as a warning
|
|
// on stderr and otherwise skipped, so a broken plugin can never wedge startup —
|
|
// mirroring how newHookDispatcher and skills.Load tolerate bad input.
|
|
func activatePlugins(workspaceRoot string, registry *tools.Registry, deps appDeps, stderr io.Writer) pluginActivation {
|
|
loaded, err := deps.loadPlugins(plugins.LoadOptions{Cwd: workspaceRoot})
|
|
if err != nil {
|
|
writePluginActivationWarning(stderr, "failed to load plugins: "+err.Error())
|
|
return pluginActivation{}
|
|
}
|
|
|
|
// Load fails OPEN per plugin: a malformed manifest is recorded as a diagnostic
|
|
// and the plugin is dropped rather than aborting the whole load. Surface those
|
|
// diagnostics so a skipped plugin is never silent.
|
|
for _, diagnostic := range loaded.Diagnostics {
|
|
writePluginActivationWarning(stderr, formatLoadDiagnostic(diagnostic))
|
|
}
|
|
|
|
result := plugins.Activate(registry, loaded.Plugins, plugins.ActivateOptions{Cwd: workspaceRoot})
|
|
for _, warning := range result.Warnings {
|
|
writePluginActivationWarning(stderr, warning)
|
|
}
|
|
|
|
// Re-register the skill tool to also resolve plugin-declared skills. The core
|
|
// skill tool only reads the default skills dir; the plugin-aware replacement
|
|
// merges the default dir with the plugin skill roots (default dir wins a name
|
|
// clash), so plugin skills appear in the agent's skill list. With no plugin
|
|
// skill roots this is byte-equivalent to the default skills surface.
|
|
if len(result.SkillRoots) > 0 {
|
|
registry.Register(plugins.NewSkillTool(deps.skillsDir(), result.SkillRoots))
|
|
}
|
|
|
|
return pluginActivation{hooks: result.Hooks, skillRoots: result.SkillRoots}
|
|
}
|
|
|
|
// skillInfos resolves the reusable skills the model can load via the skill tool —
|
|
// the default skills dir merged with any plugin skill roots, the same set the
|
|
// plugin-aware skill tool resolves against — as plain data for the agent's system
|
|
// prompt. It returns nil when no skills are installed, so a skill-less run leaves
|
|
// the prompt byte-identical.
|
|
func (a pluginActivation) skillInfos(defaultDir string) []agent.SkillInfo {
|
|
merged, _ := plugins.MergedSkills(defaultDir, a.skillRoots)
|
|
if len(merged) == 0 {
|
|
return nil
|
|
}
|
|
infos := make([]agent.SkillInfo, 0, len(merged))
|
|
for _, skill := range merged {
|
|
name := strings.TrimSpace(skill.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
infos = append(infos, agent.SkillInfo{Name: name, Description: strings.TrimSpace(skill.Description)})
|
|
}
|
|
return infos
|
|
}
|
|
|
|
// formatLoadDiagnostic renders a plugin load diagnostic into a single warning
|
|
// line, prefixing the diagnostic kind and appending the most specific locator
|
|
// available (manifest path, then plugin path, then plugin ID) so an operator can
|
|
// locate the offending plugin even when the manifest path is unknown.
|
|
func formatLoadDiagnostic(diagnostic plugins.Diagnostic) string {
|
|
message := fmt.Sprintf("[%s] %s", diagnostic.Kind, diagnostic.Message)
|
|
locator := diagnostic.ManifestPath
|
|
if locator == "" {
|
|
locator = diagnostic.PluginPath
|
|
}
|
|
if locator == "" {
|
|
locator = diagnostic.PluginID
|
|
}
|
|
if locator != "" {
|
|
message += " (" + locator + ")"
|
|
}
|
|
return message
|
|
}
|
|
|
|
func writePluginActivationWarning(stderr io.Writer, message string) {
|
|
if stderr == nil {
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(stderr, "[zero] WARNING: plugin activation: %s\n", message)
|
|
}
|