v0.0.50 3e59a3c

zotcore.go RawHistoryFolder

1// Package zotcore is the public Go SDK for embedding the zot agent
2// runtime in third-party programs. It is the only stable, importable
3// surface the project exposes; everything under internal/ is subject
4// to change without notice.
5//
6// Lifecycle:
7//
8//	rt, err := zotcore.New(zotcore.Config{Provider: "anthropic", Model: "claude-sonnet-4-5"})
9//	if err != nil { ... }
10//	defer rt.Close()
11//
12//	events, err := rt.Prompt(ctx, "fix the failing test", nil)
13//	if err != nil { ... }
14//	for ev := range events {
15//	    fmt.Printf("%s: %+v\n", ev.Type(), ev)
16//	}
17//
18// Concurrency model: one prompt at a time per Runtime. Spawn one
19// Runtime per project / cwd. The Cancel call interrupts the active
20// prompt; subsequent prompts work normally.
21//
22// For a non-Go consumer, run `zot rpc` and speak the same JSON
23// schema over stdin/stdout. See docs/rpc.md.
24package zotcore
25
26import (
27	"context"
28	"errors"
29	"fmt"
30	"sync"
31
32	"github.com/patriceckhart/zot/internal/agent"
33	"github.com/patriceckhart/zot/internal/core"
34	"github.com/patriceckhart/zot/internal/provider"
35)
36
37// Config configures a Runtime. All fields are optional; sensible
38// defaults are read from $ZOT_HOME/config.json, env vars, and the
39// resolver chain (the same one the cli uses).
40type Config struct {
41	// Provider is "anthropic" or "openai". Empty = use the user's
42	// default from config.json or env.
43	Provider string
44
45	// Model is the model id. Empty = use the provider's default.
46	Model string
47
48	// CWD is the working directory the agent operates in. Tools are
49	// confined to this directory when Lock is true. Empty = current
50	// process cwd.
51	CWD string
52
53	// SystemPrompt overrides the built-in system prompt.
54	SystemPrompt string
55
56	// AppendSystemPrompt is appended to the built-in (or overridden)
57	// system prompt. Useful for project-specific instructions.
58	AppendSystemPrompt []string
59
60	// Reasoning sets the reasoning effort for models that support it
61	// ("low", "medium", "high"). Empty = no reasoning.
62	Reasoning string
63
64	// MaxSteps caps the agent loop iterations per Prompt call.
65	// 0 uses the default (50).
66	MaxSteps int
67
68	// APIKey overrides the credential lookup chain.
69	APIKey string
70
71	// BaseURL overrides the provider base url (for tests / proxies).
72	BaseURL string
73
74	// Tools is the list of tools to enable. Nil/empty = all four
75	// (read, write, edit, bash). Pass an empty-but-non-nil slice
76	// (e.g. []string{}) plus NoTools=true to disable everything.
77	Tools []string
78
79	// NoTools disables every tool. Useful for chat-only embeddings.
80	NoTools bool
81
82	// Lock confines tools to CWD. Same effect as the /lock command.
83	Lock bool
84}
85
86// Runtime is one zot agent session. Safe for use from one goroutine
87// at a time per Runtime; create separate Runtimes for parallel work.
88type Runtime struct {
89	mu       sync.Mutex
90	agent    *core.Agent
91	provider string
92	model    string
93	cwd      string
94
95	// activeCancel is set while a Prompt is streaming.
96	activeCancel context.CancelFunc
97
98	// closed signals that Close has been called.
99	closed bool
100}
101
102// New constructs a Runtime from cfg. Returns an error if no
103// credential is available for the requested provider.
104func New(cfg Config) (*Runtime, error) {
105	args := agent.Args{
106		Mode:               agent.ModeJSON, // headless
107		Provider:           cfg.Provider,
108		Model:              cfg.Model,
109		CWD:                cfg.CWD,
110		APIKey:             cfg.APIKey,
111		BaseURL:            cfg.BaseURL,
112		SystemPrompt:       cfg.SystemPrompt,
113		AppendSystemPrompt: cfg.AppendSystemPrompt,
114		Reasoning:          cfg.Reasoning,
115		MaxSteps:           cfg.MaxSteps,
116		Tools:              cfg.Tools,
117		NoTools:            cfg.NoTools,
118		NoSess:             true, // SDK callers manage persistence themselves
119	}
120	if args.MaxSteps == 0 {
121		args.MaxSteps = 50
122	}
123	r, err := agent.Resolve(args, true)
124	if err != nil {
125		return nil, err
126	}
127	if cfg.Lock && r.Sandbox != nil {
128		r.Sandbox.Lock()
129	}
130	ag := r.NewAgent()
131	return &Runtime{
132		agent:    ag,
133		provider: r.Provider,
134		model:    r.Model,
135		cwd:      r.CWD,
136	}, nil
137}
138
139// Provider returns the active provider id.
140func (r *Runtime) Provider() string { r.mu.Lock(); defer r.mu.Unlock(); return r.provider }
141
142// Model returns the active model id.
143func (r *Runtime) Model() string { r.mu.Lock(); defer r.mu.Unlock(); return r.model }
144
145// CWD returns the working directory the agent operates in.
146func (r *Runtime) CWD() string { r.mu.Lock(); defer r.mu.Unlock(); return r.cwd }
147
148// Messages returns a copy of the current transcript.
149func (r *Runtime) Messages() []Message {
150	if r.agent == nil {
151		return nil
152	}
153	src := r.agent.Messages()
154	out := make([]Message, len(src))
155	for i, m := range src {
156		out[i] = Message{
157			Role:    string(m.Role),
158			Content: convertContent(m.Content),
159			Time:    m.Time.Format("2006-01-02T15:04:05Z"),
160		}
161	}
162	return out
163}
164
165// SetMessages replaces the transcript. Use to seed history from a
166// session file or to clear with nil.
167func (r *Runtime) SetMessages(msgs []Message) {
168	if r.agent == nil {
169		return
170	}
171	out := make([]provider.Message, 0, len(msgs))
172	for _, m := range msgs {
173		out = append(out, provider.Message{
174			Role:    provider.Role(m.Role),
175			Content: rebuildContent(m.Content),
176		})
177	}
178	r.agent.SetMessages(out)
179}
180
181// Cost returns the cumulative usage and dollar cost for this Runtime.
182func (r *Runtime) Cost() Usage {
183	if r.agent == nil {
184		return Usage{}
185	}
186	u := r.agent.Cost()
187	return Usage{
188		Input:      u.InputTokens,
189		Output:     u.OutputTokens,
190		CacheRead:  u.CacheReadTokens,
191		CacheWrite: u.CacheWriteTokens,
192		CostUSD:    u.CostUSD,
193	}
194}
195
196// Prompt sends a user message and runs the agent loop. Returns a
197// channel that emits one Event per agent action, closed when the
198// turn finishes (cleanly or with error). Only one Prompt may be
199// active at a time per Runtime; concurrent calls return ErrBusy.
200func (r *Runtime) Prompt(ctx context.Context, text string, images []Image) (<-chan Event, error) {
201	r.mu.Lock()
202	if r.closed {
203		r.mu.Unlock()
204		return nil, ErrClosed
205	}
206	if r.activeCancel != nil {
207		r.mu.Unlock()
208		return nil, ErrBusy
209	}
210	if r.agent == nil {
211		r.mu.Unlock()
212		return nil, fmt.Errorf("zotcore: no agent (login first via /login or set credentials)")
213	}
214	subCtx, cancel := context.WithCancel(ctx)
215	r.activeCancel = cancel
216	r.mu.Unlock()
217
218	imgBlocks := make([]provider.ImageBlock, 0, len(images))
219	for _, img := range images {
220		imgBlocks = append(imgBlocks, provider.ImageBlock{MimeType: img.MimeType, Data: img.Data})
221	}
222
223	out := make(chan Event, 16)
224	go func() {
225		defer close(out)
226		defer func() {
227			r.mu.Lock()
228			r.activeCancel = nil
229			r.mu.Unlock()
230		}()
231		err := r.agent.Prompt(subCtx, text, imgBlocks, func(ev core.AgentEvent) {
232			out <- toEvent(ev)
233		})
234		if err != nil && !errors.Is(err, context.Canceled) {
235			out <- Event{Type: "error", Error: err.Error()}
236		}
237	}()
238	return out, nil
239}
240
241// Cancel interrupts the active Prompt, if any. Safe to call when no
242// prompt is in flight (no-op).
243func (r *Runtime) Cancel() {
244	r.mu.Lock()
245	cancel := r.activeCancel
246	r.mu.Unlock()
247	if cancel != nil {
248		cancel()
249	}
250}
251
252// Compact summarises the current transcript into a single synthetic
253// user message, freeing up context. Blocks until done.
254func (r *Runtime) Compact(ctx context.Context, customInstructions string) (CompactResult, error) {
255	if r.agent == nil {
256		return CompactResult{}, fmt.Errorf("zotcore: no agent")
257	}
258	r.mu.Lock()
259	if r.activeCancel != nil {
260		r.mu.Unlock()
261		return CompactResult{}, ErrBusy
262	}
263	subCtx, cancel := context.WithCancel(ctx)
264	r.activeCancel = cancel
265	r.mu.Unlock()
266	defer func() {
267		r.mu.Lock()
268		r.activeCancel = nil
269		r.mu.Unlock()
270	}()
271
272	summary, err := r.agent.Compact(subCtx, 4, nil)
273	if err != nil {
274		return CompactResult{}, err
275	}
276	return CompactResult{Summary: summary, Messages: r.Messages()}, nil
277}
278
279// SetModel switches the active model. Same provider only; for cross-
280// provider switches, create a new Runtime.
281func (r *Runtime) SetModel(model string) error {
282	r.mu.Lock()
283	defer r.mu.Unlock()
284	if r.agent == nil {
285		return fmt.Errorf("zotcore: no agent")
286	}
287	if _, err := provider.FindModel(r.provider, model); err != nil {
288		return err
289	}
290	r.agent.Model = model
291	r.model = model
292	return nil
293}
294
295// State returns a snapshot of the runtime's current state. Useful for
296// driving UIs.
297func (r *Runtime) State() State {
298	r.mu.Lock()
299	defer r.mu.Unlock()
300	return State{
301		Provider:     r.provider,
302		Model:        r.model,
303		CWD:          r.cwd,
304		Busy:         r.activeCancel != nil,
305		MessageCount: len(r.Messages()),
306	}
307}
308
309// Close releases resources held by the Runtime. Cancels any active
310// prompt. Subsequent calls return ErrClosed.
311func (r *Runtime) Close() error {
312	r.mu.Lock()
313	if r.closed {
314		r.mu.Unlock()
315		return nil
316	}
317	r.closed = true
318	cancel := r.activeCancel
319	r.mu.Unlock()
320	if cancel != nil {
321		cancel()
322	}
323	return nil
324}
325
326// ListModels returns every model known to the runtime for the
327// current provider (catalog + live discovery if cached).
328func (r *Runtime) ListModels() []ModelInfo {
329	models := provider.ModelsForProvider(r.Provider())
330	out := make([]ModelInfo, 0, len(models))
331	for _, m := range models {
332		out = append(out, ModelInfo{
333			ID:            m.ID,
334			Provider:      m.Provider,
335			ContextWindow: m.ContextWindow,
336			MaxOutput:     m.MaxOutput,
337			Reasoning:     m.Reasoning,
338		})
339	}
340	return out
341}
342
343// ---- public errors ----
344
345// ErrBusy is returned when a Prompt or Compact is started while
346// another is in flight on the same Runtime.
347var ErrBusy = errors.New("zotcore: runtime is busy")
348
349// ErrClosed is returned by methods on a Runtime after Close.
350var ErrClosed = errors.New("zotcore: runtime is closed")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close