1// Package core implements the agent loop, tool runtime, and session
2// persistence. It is provider-agnostic: it talks to an LLM only through
3// the provider.Client interface.
4package core
5
6import (
7 "context"
8 "encoding/json"
9 "fmt"
10 "sort"
11
12 "github.com/bnema/zut/packages/provider"
13)
14
15// Tool is a capability the agent can invoke.
16type Tool interface {
17 // Name is the unique tool id shown to the LLM.
18 Name() string
19 // Description is a one-line summary shown to the LLM.
20 Description() string
21 // Schema is a JSON Schema object for Execute's args.
22 Schema() json.RawMessage
23 // Execute runs the tool. progress may be called any number of times
24 // with partial textual output (for UIs); it is not sent to the LLM.
25 Execute(ctx context.Context, args json.RawMessage, progress func(string)) (ToolResult, error)
26}
27
28// ToolPreviewer is optionally implemented by tools that can describe their
29// exact effect without applying it. Confirmation UIs use the preview before
30// allowing a side-effecting call to proceed.
31type ToolPreviewer interface {
32 Preview(ctx context.Context, args json.RawMessage) (ToolResult, error)
33}
34
35// ToolArgumentRewritePolicy optionally prevents extension guards from
36// replacing a tool call's arguments before execution.
37type ToolArgumentRewritePolicy interface {
38 AllowArgumentRewrite() bool
39}
40
41// ToolResult is the outcome of Tool.Execute.
42type ToolResult struct {
43 // Content is sent back to the LLM (text and/or images).
44 Content []provider.Content
45 // IsError marks this result as an error to the LLM.
46 IsError bool
47 // ActivateTools names previously deferred tools that become available
48 // after this result. Unknown names are ignored by the agent.
49 ActivateTools []string
50 // Details is arbitrary data for UIs and logs; not sent to the LLM.
51 Details any
52 // Timing is populated by the core invocation boundary. It is also copied
53 // to the persisted/provider-facing tool-result block, where it remains
54 // optional for compatibility with older sessions.
55 Timing *provider.ToolTiming
56 // Context describes workspace resources read, discovered, or mutated by
57 // this result. Core uses it only for provider-visible stale-result pruning.
58 Context provider.ToolContext
59 // PlanOperation is set by core when a plan call passed validation and is
60 // pending commit. It is applied to agent state only after CommitToolResult
61 // succeeds.
62 PlanOperation *PlanOperation
63}
64
65// Registry is a name->Tool map.
66type Registry map[string]Tool
67
68// NewRegistry builds a Registry from a list of tools.
69func NewRegistry(tools ...Tool) Registry {
70 r := Registry{}
71 for _, t := range tools {
72 r[t.Name()] = t
73 }
74 return r
75}
76
77// Specs returns the tool definitions to advertise to the LLM.
78// Sorted by tool name so the order is stable across requests. This
79// is load-bearing for provider-side prompt caching: providers
80// prefix-match tool definitions, and Go's map iteration order is
81// randomized per call, which would otherwise bust the cache every
82// single turn.
83func (r Registry) Specs() []provider.Tool {
84 names := make([]string, 0, len(r))
85 for name := range r {
86 names = append(names, name)
87 }
88 sort.Strings(names)
89 out := make([]provider.Tool, 0, len(r))
90 for _, name := range names {
91 t := r[name]
92 deferred := false
93 if d, ok := t.(interface{ Deferred() bool }); ok {
94 deferred = d.Deferred()
95 }
96 out = append(out, provider.Tool{
97 Name: t.Name(),
98 Description: t.Description(),
99 Schema: t.Schema(),
100 Deferred: deferred,
101 })
102 }
103 return out
104}
105
106// Get looks up a tool by name.
107func (r Registry) Get(name string) (Tool, error) {
108 t, ok := r[name]
109 if !ok {
110 return nil, fmt.Errorf("unknown tool %q", name)
111 }
112 return t, nil
113}