Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/HOW_ZERO_WORKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ flowchart TD
- **Plugins** can add tools, hooks, and skill roots. Bootstrap always registers a
multi-root skill tool: primary Zero skills dir, optional `~/.agents/skills`,
then plugin skill roots (earlier wins). `internal/skills` owns that merge via
`LoadFromRoots` / `DiscoveryRoots`; single-root `Load` remains for install/write.
`LoadFromRoots`; single-root `Load` remains for install/write.
- **Hooks** can observe or block tool lifecycle events.

## End-to-End Data Flow
Expand Down
3 changes: 0 additions & 3 deletions internal/acp/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ func NewAgent(conn *Conn, deps Deps) *Agent {
return a
}

// Serve runs the connection read loop until the stream closes or ctx is done.
func (a *Agent) Serve(ctx context.Context) error { return a.conn.Serve(ctx) }

// ---- initialize ----

func (a *Agent) handleInitialize(_ context.Context, params json.RawMessage) (any, error) {
Expand Down
11 changes: 11 additions & 0 deletions internal/acp/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Test seams: helpers only test code uses, kept out of the production binary.
package acp

import "context"

// Serve runs the connection read loop until the stream closes or ctx is done.
func (a *Agent) Serve(ctx context.Context) error { return a.conn.Serve(ctx) }

func ImageBlock(base64Data, mimeType string) ContentBlock {
return ContentBlock{Type: "image", Data: base64Data, MimeType: mimeType}
}
8 changes: 0 additions & 8 deletions internal/acp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ type ContentBlock struct {

func TextBlock(text string) ContentBlock { return ContentBlock{Type: "text", Text: text} }

func ImageBlock(base64Data, mimeType string) ContentBlock {
return ContentBlock{Type: "image", Data: base64Data, MimeType: mimeType}
}

// ---- sessions ----

// McpServer mirrors the editor-provided MCP server entry. ZERO owns its own MCP
Expand Down Expand Up @@ -233,10 +229,6 @@ func ToolContent(block ContentBlock) ToolCallContent {
return ToolCallContent{Type: "content", Content: &block}
}

func ToolDiff(path, oldText, newText string) ToolCallContent {
return ToolCallContent{Type: "diff", Path: path, OldText: oldText, NewText: newText}
}

type ToolCallLocation struct {
Path string `json:"path"`
Line *int `json:"line,omitempty"`
Expand Down
8 changes: 4 additions & 4 deletions internal/agent/additional_permissions_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func TestExecuteToolCallRejectsMissingAdditionalPermissionsBeforePrompt(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewBashTool(root))
registry.Register(tools.NewScopedBashTool(root, nil))

promptCalls := 0
result, err := executeToolCall(
Expand Down Expand Up @@ -51,7 +51,7 @@ func TestExecuteToolCallRejectsMissingAdditionalPermissionsBeforePrompt(t *testi
func TestExecuteToolCallRejectsAdditionalPermissionsWithoutFlagBeforePrompt(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewBashTool(root))
registry.Register(tools.NewScopedBashTool(root, nil))

promptCalls := 0
result, err := executeToolCall(
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestExecuteToolCallRejectsAdditionalPermissionsWithoutFlagBeforePrompt(t *t
func TestExecuteToolCallMissingAdditionalPermissionsErrorIsActionable(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewBashTool(root))
registry.Register(tools.NewScopedBashTool(root, nil))

result, err := executeToolCall(
context.Background(),
Expand Down Expand Up @@ -117,7 +117,7 @@ func TestExecuteToolCallMissingAdditionalPermissionsErrorIsActionable(t *testing
func TestExecuteToolCallStillPromptsForValidAdditionalPermissions(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewBashTool(root))
registry.Register(tools.NewScopedBashTool(root, nil))

promptCalls := 0
_, err := executeToolCall(
Expand Down
12 changes: 0 additions & 12 deletions internal/agent/command_prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,3 @@ func hasStringPrefix(values []string, prefix []string) bool {
}
return true
}

func equalStringSlices(left []string, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
9 changes: 0 additions & 9 deletions internal/agent/compaction_preserve.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,6 @@ func formatPreservedState(plan string, task *preservedTaskState, edits, tools, s
return preservedStateLabel + "\n" + string(encoded)
}

// parsePreservedState recovers the plan + skills from a prior summary's preserved
// block. JSON escaping makes this lossless even when a skill body contains
// markdown headings, code fences, or quotes. Returns ("", nil) when absent or
// malformed.
func parsePreservedState(summaryContent string) (string, []skillEntry) {
state := parsePreservedStateBlock(summaryContent)
return state.Plan, preservedSkillsToEntries(state.Skills)
}

func parsePreservedStateBlock(summaryContent string) preservedState {
idx := strings.LastIndex(summaryContent, preservedStateLabel)
if idx < 0 {
Expand Down
8 changes: 4 additions & 4 deletions internal/agent/compaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ func TestRunProactiveCompactionTriggers(t *testing.T) {
}

registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(t.TempDir()))
registry.Register(tools.NewScopedReadFileTool(t.TempDir(), nil))

result, err := Run(context.Background(), strings.Repeat("y", 8000), provider, Options{
Registry: registry,
Expand Down Expand Up @@ -364,7 +364,7 @@ func TestRunNoCompactionWhenContextWindowZero(t *testing.T) {
},
}
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(t.TempDir()))
registry.Register(tools.NewScopedReadFileTool(t.TempDir(), nil))

_, err := Run(context.Background(), strings.Repeat("y", 8000), provider, Options{
Registry: registry,
Expand Down Expand Up @@ -427,7 +427,7 @@ func TestRunReactiveCompactionRecovers(t *testing.T) {
// the compaction closure. read_file also returns a sizeable result that
// bloats the history.
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(t.TempDir()))
registry.Register(tools.NewScopedReadFileTool(t.TempDir(), nil))

// ContextWindow large enough that proactive compaction never triggers, so
// only the reactive path can save the run.
Expand Down Expand Up @@ -491,7 +491,7 @@ func TestRunReactiveRetryDoesNotDoubleEmitText(t *testing.T) {
finalText: "recovered",
}
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(t.TempDir()))
registry.Register(tools.NewScopedReadFileTool(t.TempDir(), nil))

var deltas []string
result, err := Run(context.Background(), strings.Repeat("z", 6000), provider, Options{
Expand Down
6 changes: 3 additions & 3 deletions internal/agent/deferred_loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func assertNoDeferredDiscoveryMessage(t *testing.T, request zeroruntime.Completi
func TestPartitionToolsInactiveIsByteIdenticalAndDropsToolSearch(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))
registry.Register(fakeDeferredTool{name: "mcp__srv__a", desc: "tool a"})
registry.Register(fakeToolSearchTool{})

Expand Down Expand Up @@ -305,8 +305,8 @@ func TestPartitionToolsActiveExcludesDisabledDeferredFromDiscoveryAndExposed(t *
func TestPartitionToolsActiveHidesUnloadedExposesLoaded(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root)) // non-deferred builtin
registry.Register(fakeToolSearchTool{}) // non-deferred, must stay exposed
registry.Register(tools.NewScopedReadFileTool(root, nil)) // non-deferred builtin
registry.Register(fakeToolSearchTool{}) // non-deferred, must stay exposed
registry.Register(fakeDeferredTool{name: "mcp__srv__alpha", desc: "alpha tool"})
registry.Register(fakeDeferredTool{name: "mcp__srv__beta", desc: "beta tool"})

Expand Down
2 changes: 1 addition & 1 deletion internal/agent/denial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func TestExecuteToolCallCategorizesFilteredDenial(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))

result, _ := executeToolCall(context.Background(), registry, ToolCall{ID: "c1", Name: "read_file", Arguments: `{"path":"x"}`}, PermissionModeAuto, Options{
DisabledTools: []string{"read_file"},
Expand Down
41 changes: 41 additions & 0 deletions internal/agent/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Test seams: helpers only test code uses, kept out of the production binary.
package agent

import (
"github.com/Gitlawb/zero/internal/tools"
"github.com/Gitlawb/zero/internal/zeroruntime"
)

func equalStringSlices(left []string, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}

// parsePreservedState recovers the plan + skills from a prior summary's preserved
// block. JSON escaping makes this lossless even when a skill body contains
// markdown headings, code fences, or quotes. Returns ("", nil) when absent or
// malformed.
func parsePreservedState(summaryContent string) (string, []skillEntry) {
state := parsePreservedStateBlock(summaryContent)
return state.Plan, preservedSkillsToEntries(state.Skills)
}

// partitionTools builds the per-turn advertised tool list and optional
// tool_search discovery text. INACTIVE (DeferThreshold <= 0 or the eligible count is
// below it): every visible tool is exposed with its full schema EXCEPT tool_search
// (dropped so it is never advertised when it cannot help), and the discovery text is
// empty — byte-identical to the pre-deferral output. ACTIVE: a deferred-eligible
// tool is exposed only when loaded[name]; otherwise it is hidden and searchable
// through tool_search. Non-deferred tools (including tool_search) are always
// exposed. The exposed slice is alpha-sorted by name, matching the legacy order
// so the inactive path is stable.
func partitionTools(registry *tools.Registry, permissionMode PermissionMode, options Options, loaded map[string]bool) ([]zeroruntime.ToolDefinition, string) {
return partitionToolsCached(registry, permissionMode, options, loaded, nil)
}
14 changes: 7 additions & 7 deletions internal/agent/guardrails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestRunResetsEmptyTurnCounterOnToolCall(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))

provider := &mockProvider{
turns: [][]zeroruntime.StreamEvent{
Expand Down Expand Up @@ -260,7 +260,7 @@ func TestRunInjectsPlanNotCalledReminderForMultiStepTask(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))

provider := &mockProvider{
turns: [][]zeroruntime.StreamEvent{
Expand Down Expand Up @@ -291,7 +291,7 @@ func TestRunDoesNotInjectPlanReminderForTrivialTask(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))

provider := &mockProvider{
turns: [][]zeroruntime.StreamEvent{
Expand Down Expand Up @@ -319,7 +319,7 @@ func TestRunDoesNotInjectNotCalledReminderWhenPlanUsed(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))
registry.Register(tools.NewUpdatePlanTool())

provider := &mockProvider{
Expand Down Expand Up @@ -349,7 +349,7 @@ func TestRunInjectsStalePlanReminderAfterManyToolCalls(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))
registry.Register(tools.NewUpdatePlanTool())

// Turn 1 calls update_plan (so the not-called reminder never triggers), then
Expand Down Expand Up @@ -383,7 +383,7 @@ func TestRunStalePlanReminderIsOneShotPerInterval(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))
registry.Register(tools.NewUpdatePlanTool())

turns := [][]zeroruntime.StreamEvent{
Expand Down Expand Up @@ -418,7 +418,7 @@ func TestRunInjectsToolOnlyProgressReminder(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, root+"/notes.txt", "alpha")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
registry.Register(tools.NewScopedReadFileTool(root, nil))

turns := make([][]zeroruntime.StreamEvent, 0, toolOnlyProgressReminderAt+1)
for i := 0; i < toolOnlyProgressReminderAt; i++ {
Expand Down
17 changes: 0 additions & 17 deletions internal/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,6 @@ func Run(ctx context.Context, prompt string, provider Provider, options Options)
})
}

// Build the per-turn tool list first so proactive compaction can include
// the tool-definition tokens (they ride on every request) in its estimate.
// partitionTools depends only on registry/permissions/options/loaded, not on
// the messages, so computing it before compaction is safe.
// Build the per-turn tool list first so proactive compaction can include
// the tool-definition tokens (they ride on every request) in its estimate.
// partitionTools depends only on registry/permissions/options/loaded, not on
Expand Down Expand Up @@ -2866,19 +2862,6 @@ func permissionActionFromSandbox(action sandbox.Action) PermissionAction {
}
}

// partitionTools builds the per-turn advertised tool list and optional
// tool_search discovery text. INACTIVE (DeferThreshold <= 0 or the eligible count is
// below it): every visible tool is exposed with its full schema EXCEPT tool_search
// (dropped so it is never advertised when it cannot help), and the discovery text is
// empty — byte-identical to the pre-deferral output. ACTIVE: a deferred-eligible
// tool is exposed only when loaded[name]; otherwise it is hidden and searchable
// through tool_search. Non-deferred tools (including tool_search) are always
// exposed. The exposed slice is alpha-sorted by name, matching the legacy order
// so the inactive path is stable.
func partitionTools(registry *tools.Registry, permissionMode PermissionMode, options Options, loaded map[string]bool) ([]zeroruntime.ToolDefinition, string) {
return partitionToolsCached(registry, permissionMode, options, loaded, nil)
}

// partitionToolsCached is partitionTools with an optional per-tool definition
// cache. The partitioning itself (visibility, deferral, ordering) is recomputed
// every call — it must be, because a tool's deferred state can flip mid-run (e.g.
Expand Down
Loading
Loading