1// Package extproto defines the JSON-over-stdin/stdout wire format
2// spoken between zot and its extension subprocesses. Both the host
3// (internal/agent/extensions) and the SDK (pkg/zotext) marshal/
4// unmarshal the same types, so changes here ripple through both.
5//
6// All frames are one JSON object terminated by a single LF. Object
7// boundaries follow newline boundaries; no multi-line JSON.
8//
9// Direction conventions in this file:
10// - Type names ending in "FromExt" are sent by the extension to zot.
11// - Type names ending in "FromHost" are sent by zot to the extension.
12// - Names without a suffix are direction-neutral payloads or shared
13// value types.
14//
15// Every frame has a top-level Type discriminator. Optional ID is
16// present on commands and on responses to commands so the sender can
17// correlate; events and notifications never carry an ID.
18package extproto
19
20import "encoding/json"
21
22// ProtocolVersion is the major version of this wire format. Bumped
23// for breaking changes; minor additions don't bump.
24const ProtocolVersion = 1
25
26// Frame is the lowest-common-denominator parse target so a reader can
27// peek at the type before unmarshalling the full payload.
28type Frame struct {
29 Type string `json:"type"`
30 ID string `json:"id,omitempty"`
31}
32
33// ---- extension -> host ----
34
35// HelloFromExt is the first frame the extension sends after start.
36// Zot replies with HelloAckFromHost, then registration frames
37// (RegisterCommandFromExt, etc.) flow.
38type HelloFromExt struct {
39 Type string `json:"type"` // "hello"
40 Name string `json:"name"`
41 Version string `json:"version"`
42 Capabilities []string `json:"capabilities,omitempty"`
43}
44
45// RegisterCommandFromExt asks zot to bind /name to this extension.
46// Description appears in the slash autocomplete + /help.
47type RegisterCommandFromExt struct {
48 Type string `json:"type"` // "register_command"
49 Name string `json:"name"`
50 Description string `json:"description,omitempty"`
51}
52
53// RegisterToolFromExt asks zot to expose a tool to the LLM. The
54// schema is a JSON Schema object describing Args; zot doesn't validate
55// the model's arguments against it (the model providers do that), but
56// it must parse as valid JSON or registration is rejected.
57//
58// Tool names live in the same namespace as built-in tools (read,
59// write, edit, bash, skill). Conflicts are silently shadowed by the
60// built-in; check the extension's log for a warning.
61type RegisterToolFromExt struct {
62 Type string `json:"type"` // "register_tool"
63 Name string `json:"name"`
64 Description string `json:"description,omitempty"`
65 Schema json.RawMessage `json:"schema"`
66}
67
68// ReadyFromExt signals "all initial registrations sent". The host
69// waits for this (with a short timeout) before building the agent's
70// tool registry, so model calls don't race extension tool
71// registration.
72type ReadyFromExt struct {
73 Type string `json:"type"` // "ready"
74}
75
76// SubscribeFromExt declares which lifecycle events the extension
77// wants to observe (one-way `event` frames) and which it wants to
78// intercept (round-trip `event_intercept` frames). Send once after
79// hello, before ready.
80//
81// Recognised event names: "session_start", "turn_start",
82// "turn_end", "tool_call", "assistant_message".
83//
84// Only "tool_call" supports interception in this version; values
85// listed in Intercept that aren't "tool_call" are ignored.
86type SubscribeFromExt struct {
87 Type string `json:"type"` // "subscribe"
88 Events []string `json:"events,omitempty"`
89 Intercept []string `json:"intercept,omitempty"`
90}
91
92// EventInterceptResponseFromExt is the extension's reply to an
93// EventInterceptFromHost. block=true refuses the underlying action;
94// reason is shown to the model as the tool error message. Both
95// fields default to (false, "") meaning "allow".
96type EventInterceptResponseFromExt struct {
97 Type string `json:"type"` // "event_intercept_response"
98 ID string `json:"id"`
99 Block bool `json:"block,omitempty"`
100 Reason string `json:"reason,omitempty"`
101}
102
103// ToolResultFromExt is the extension's reply to a ToolCallFromHost.
104// Content[] follows the same shape as elsewhere in zot:
105//
106// {"type":"text", "text":"..."}
107// {"type":"image", "mime_type":"image/png", "data":"<base64>"}
108//
109// Set IsError true to mark the tool call as failed; the model sees
110// the content as the error explanation.
111type ToolResultFromExt struct {
112 Type string `json:"type"` // "tool_result"
113 ID string `json:"id"`
114 Content []ContentBlock `json:"content"`
115 IsError bool `json:"is_error,omitempty"`
116}
117
118// ContentBlock is one entry in a tool result's content array.
119type ContentBlock struct {
120 Type string `json:"type"` // "text" | "image"
121 Text string `json:"text,omitempty"`
122 MimeType string `json:"mime_type,omitempty"`
123 Data string `json:"data,omitempty"` // base64
124}
125
126// CommandResponseFromExt is the extension's answer to a
127// CommandInvokedFromHost. Action drives what zot does next:
128//
129// - "prompt" → submit Prompt as a fresh user message to the agent
130// - "insert" → insert Insert into the editor buffer at the cursor
131// - "display" → append Display to the chat as a one-shot note
132// (no model call, no transcript entry)
133// - "noop" → command handled internally, no UI change
134type CommandResponseFromExt struct {
135 Type string `json:"type"` // "command_response"
136 ID string `json:"id"`
137 Action string `json:"action"` // see above
138 Prompt string `json:"prompt,omitempty"` // for action=prompt
139 Insert string `json:"insert,omitempty"` // for action=insert
140 Display string `json:"display,omitempty"` // for action=display
141 Error string `json:"error,omitempty"` // command failed; render to user
142}
143
144// NotifyFromExt is a one-way status message the extension can push at
145// any time. Zot renders it in the chat as a styled note.
146type NotifyFromExt struct {
147 Type string `json:"type"` // "notify"
148 Level string `json:"level"` // "info" | "warn" | "error" | "success"
149 Message string `json:"message"`
150}
151
152// ShutdownAckFromExt acknowledges the host's shutdown request. The
153// extension should exit shortly after sending this; zot waits a few
154// seconds before SIGTERM.
155type ShutdownAckFromExt struct {
156 Type string `json:"type"` // "shutdown_ack"
157}
158
159// ---- host -> extension ----
160
161// HelloAckFromHost is zot's reply to HelloFromExt. The extension may
162// inspect the host version + currently-active provider/model to decide
163// whether to register particular commands.
164type HelloAckFromHost struct {
165 Type string `json:"type"` // "hello_ack"
166 ProtocolVersion int `json:"protocol_version"`
167 ZotVersion string `json:"zot_version"`
168 Provider string `json:"provider"`
169 Model string `json:"model"`
170 CWD string `json:"cwd"`
171}
172
173// CommandInvokedFromHost is sent when the user runs a slash command
174// the extension previously registered. Args contains everything after
175// the command name (already trimmed).
176type CommandInvokedFromHost struct {
177 Type string `json:"type"` // "command_invoked"
178 ID string `json:"id"`
179 Name string `json:"name"`
180 Args string `json:"args,omitempty"`
181}
182
183// ToolCallFromHost is sent when the LLM invokes a tool the extension
184// registered. Args is the raw JSON object the model produced; the
185// extension is responsible for validating/coercing it. Reply with
186// ToolResultFromExt within the host's tool timeout (default 60s).
187type ToolCallFromHost struct {
188 Type string `json:"type"` // "tool_call"
189 ID string `json:"id"`
190 Name string `json:"name"`
191 Args json.RawMessage `json:"args"`
192}
193
194// EventFromHost is a one-way lifecycle notification. The payload
195// fields populated depend on Event:
196//
197// session_start : (no extra fields)
198// turn_start : Step
199// turn_end : Stop, optional Error
200// tool_call : ToolID, ToolName, ToolArgs
201// assistant_message: Text
202type EventFromHost struct {
203 Type string `json:"type"` // "event"
204 Event string `json:"event"`
205
206 Step int `json:"step,omitempty"`
207 Stop string `json:"stop,omitempty"`
208 Error string `json:"error,omitempty"`
209
210 ToolID string `json:"tool_id,omitempty"`
211 ToolName string `json:"tool_name,omitempty"`
212 ToolArgs json.RawMessage `json:"tool_args,omitempty"`
213
214 Text string `json:"text,omitempty"`
215}
216
217// EventInterceptFromHost is sent when zot wants to give the
218// extension a chance to block / annotate a lifecycle event before
219// it happens. Same payload shape as EventFromHost. Reply with
220// EventInterceptResponseFromExt within the host's intercept timeout
221// (default 5s); missing the deadline is treated as "allow".
222//
223// Only Event="tool_call" is sent in this version.
224type EventInterceptFromHost struct {
225 Type string `json:"type"` // "event_intercept"
226 ID string `json:"id"`
227 Event string `json:"event"`
228
229 ToolID string `json:"tool_id,omitempty"`
230 ToolName string `json:"tool_name,omitempty"`
231 ToolArgs json.RawMessage `json:"tool_args,omitempty"`
232}
233
234// ShutdownFromHost asks the extension to clean up and exit. Zot
235// sends this when the user runs /reload-ext or zot itself is exiting
236// gracefully. Extensions that don't reply within a few seconds get
237// SIGTERM; SIGKILL after a few more.
238type ShutdownFromHost struct {
239 Type string `json:"type"` // "shutdown"
240}
241
242// ---- error frame (either direction) ----
243
244// Error is a generic failure response. Used by either side when a
245// frame can't be processed (malformed JSON, unknown type, etc.).
246type Error struct {
247 Type string `json:"type"` // "error"
248 ID string `json:"id,omitempty"`
249 Message string `json:"message"`
250}
251
252// ---- helpers ----
253
254// Encode marshals v and appends a trailing LF, ready to write to the
255// peer's pipe. Returns the marshalling error, if any.
256func Encode(v any) ([]byte, error) {
257 b, err := json.Marshal(v)
258 if err != nil {
259 return nil, err
260 }
261 return append(b, '\n'), nil
262}