1// Package extproto defines the JSON-over-stdin/stdout wire format
2// spoken between zut and its extension subprocesses. Both the host
3// (packages/agent/extensions) and the SDK (packages/agent/ext) 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 zut.
11// - Type names ending in "FromHost" are sent by zut 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 (
21 "encoding/json"
22 "strings"
23)
24
25// ProtocolVersion is the extension protocol major. Version 2 introduces the
26// zut_version wire key and requires both sides to reject mismatched majors.
27const ProtocolVersion = 2
28
29type Frame struct {
30 Type string `json:"type"`
31 ID string `json:"id,omitempty"`
32}
33
34type HelloFromExt struct {
35 Type string `json:"type"`
36 ProtocolVersion int `json:"protocol_version"`
37 Name string `json:"name"`
38 Version string `json:"version"`
39 Capabilities []string `json:"capabilities,omitempty"`
40}
41
42type RegisterCommandFromExt struct {
43 Type string `json:"type"`
44 Name string `json:"name"`
45 Description string `json:"description,omitempty"`
46}
47
48type RegisterToolFromExt struct {
49 Type string `json:"type"`
50 Name string `json:"name"`
51 Description string `json:"description,omitempty"`
52 Schema json.RawMessage `json:"schema"`
53 Deferred bool `json:"deferred,omitempty"`
54}
55
56type ReadyFromExt struct {
57 Type string `json:"type"`
58}
59
60type SubscribeFromExt struct {
61 Type string `json:"type"`
62 Events []string `json:"events,omitempty"`
63 Intercept []string `json:"intercept,omitempty"`
64}
65
66type EventInterceptResponseFromExt struct {
67 Type string `json:"type"`
68 ID string `json:"id"`
69 Block bool `json:"block,omitempty"`
70 Reason string `json:"reason,omitempty"`
71 ModifiedArgs json.RawMessage `json:"modified_args,omitempty"`
72 ReplaceText string `json:"replace_text,omitempty"`
73 // Context is hidden, bounded context for the upcoming model turn. It is
74 // not appended to the transcript or shown as a user message.
75 Context string `json:"context,omitempty"`
76}
77
78type ToolResultFromExt struct {
79 Type string `json:"type"`
80 ID string `json:"id"`
81 Content []ContentBlock `json:"content"`
82 IsError bool `json:"is_error,omitempty"`
83 ActivateTools []string `json:"activate_tools,omitempty"`
84 // Details is extension-owned metadata. The host never includes it in
85 // provider requests, but may persist it with the active session.
86 Details json.RawMessage `json:"details,omitempty"`
87}
88
89type ContentBlock struct {
90 Type string `json:"type"`
91 Text string `json:"text,omitempty"`
92 MimeType string `json:"mime_type,omitempty"`
93 Data string `json:"data,omitempty"`
94}
95
96type CommandResponseFromExt struct {
97 Type string `json:"type"`
98 ID string `json:"id"`
99 Action string `json:"action"`
100 Prompt string `json:"prompt,omitempty"`
101 Insert string `json:"insert,omitempty"`
102 Display string `json:"display,omitempty"`
103 OpenPanel *PanelSpec `json:"open_panel,omitempty"`
104 Error string `json:"error,omitempty"`
105}
106
107type PanelSpec struct {
108 ID string `json:"id"`
109 Title string `json:"title,omitempty"`
110 Lines []string `json:"lines,omitempty"`
111 Footer string `json:"footer,omitempty"`
112}
113
114// OpenPanelFromExt is a spontaneous one-way frame an extension can send at
115// any time to open an interactive panel. Unlike the open_panel action inside
116// CommandResponseFromExt, this form is uncoupled from any command invocation
117// and may be sent from a tool handler goroutine or any background context.
118type OpenPanelFromExt struct {
119 Type string `json:"type"` // "open_panel"
120 Panel PanelSpec `json:"panel"`
121}
122
123type PanelRenderFromExt struct {
124 Type string `json:"type"`
125 PanelID string `json:"panel_id"`
126 Title string `json:"title,omitempty"`
127 Lines []string `json:"lines,omitempty"`
128 Footer string `json:"footer,omitempty"`
129}
130
131type PanelCloseFromExt struct {
132 Type string `json:"type"`
133 PanelID string `json:"panel_id"`
134}
135
136// StatusFromExt sets or replaces one persistent status item owned by an
137// extension. An empty text clears the item.
138type StatusFromExt struct {
139 Type string `json:"type"`
140 Key string `json:"key"`
141 Level string `json:"level,omitempty"`
142 Text string `json:"text,omitempty"`
143}
144
145const (
146 WidgetPositionAboveInput = "above_input"
147 WidgetPositionRightBar = "right_bar"
148)
149
150// NormalizeWidgetPosition returns the host's canonical widget placement.
151// Empty and unknown values intentionally retain the historical above-input
152// behavior so older extensions and future placement hints remain compatible.
153func NormalizeWidgetPosition(position string) string {
154 switch strings.ToLower(strings.TrimSpace(position)) {
155 case WidgetPositionRightBar:
156 return WidgetPositionRightBar
157 default:
158 return WidgetPositionAboveInput
159 }
160}
161
162// WidgetFromExt sets or replaces one persistent widget owned by an
163// extension. Position is a host-defined placement hint. Interactive hosts
164// support "above_input" and "right_bar"; empty and unknown values retain
165// the historical "above_input" behavior.
166type WidgetFromExt struct {
167 Type string `json:"type"`
168 ID string `json:"id"`
169 Position string `json:"position,omitempty"`
170 Title string `json:"title,omitempty"`
171 Lines []string `json:"lines,omitempty"`
172}
173
174type ClearWidgetFromExt struct {
175 Type string `json:"type"`
176 ID string `json:"id"`
177}
178
179const AlertKindBell = "bell"
180
181// AlertRequest is a host-owned alert request. Extensions provide semantic
182// metadata; the host decides whether and how to render it.
183type AlertRequest struct {
184 Kind string `json:"kind"`
185 Reason string `json:"reason,omitempty"`
186}
187
188// AlertFromExt is a spontaneous one-way frame an extension can send at any
189// time to request a host alert.
190type AlertFromExt struct {
191 Type string `json:"type"`
192 AlertRequest
193}
194
195type NotifyFromExt struct {
196 Type string `json:"type"`
197 Level string `json:"level"`
198 Message string `json:"message"`
199}
200
201// ClearNotesFromExt is a spontaneous frame an extension can send to
202// retract every note it previously pushed via notify/display, so
203// transient status lines (e.g. an approval prompt) do not stack up
204// forever in the bottom-sticky notes block.
205type ClearNotesFromExt struct {
206 Type string `json:"type"`
207}
208
209// SubmitSlashFromExt is a spontaneous frame an extension can send at
210// any time (typically from a panel_key handler) to invoke a slash
211// command in the host's TUI as if the user had typed it. Text must
212// start with '/'. Reserved for internal / opt-in extensions today;
213// the wire format is stable but not yet exposed in the public docs.
214type SubmitSlashFromExt struct {
215 Type string `json:"type"`
216 Text string `json:"text"`
217}
218
219// SubmitFromExt is a spontaneous frame an extension can send to queue
220// a model prompt in the interactive host. The manager routes it through
221// HostHooks.Submit, so the TUI handles busy state and turn startup.
222type SubmitFromExt struct {
223 Type string `json:"type"`
224 Text string `json:"text"`
225}
226
227type ShutdownAckFromExt struct {
228 Type string `json:"type"`
229}
230
231type HelloAckFromHost struct {
232 Type string `json:"type"`
233 ProtocolVersion int `json:"protocol_version"`
234 ZutVersion string `json:"zut_version"`
235 Provider string `json:"provider"`
236 Model string `json:"model"`
237 CWD string `json:"cwd"`
238 ExtensionDir string `json:"extension_dir,omitempty"`
239 DataDir string `json:"data_dir,omitempty"`
240 Session *SessionContext `json:"session,omitempty"`
241}
242
243// SessionContext identifies the active transcript branch. ID is the current
244// session/branch id; ParentID is empty for a root session.
245type SessionContext struct {
246 ID string `json:"id"`
247 ParentID string `json:"parent_id,omitempty"`
248 Path string `json:"path,omitempty"`
249 CWD string `json:"cwd,omitempty"`
250 ForkPoint int `json:"fork_point,omitempty"`
251}
252
253type CommandInvokedFromHost struct {
254 Type string `json:"type"`
255 ID string `json:"id"`
256 Name string `json:"name"`
257 Args string `json:"args,omitempty"`
258}
259
260type ToolCallFromHost struct {
261 Type string `json:"type"`
262 ID string `json:"id"`
263 Name string `json:"name"`
264 Args json.RawMessage `json:"args"`
265}
266
267type EventFromHost struct {
268 Type string `json:"type"`
269 Event string `json:"event"`
270 Step int `json:"step,omitempty"`
271 Stop string `json:"stop,omitempty"`
272 Error string `json:"error,omitempty"`
273 ToolID string `json:"tool_id,omitempty"`
274 ToolName string `json:"tool_name,omitempty"`
275 ToolArgs json.RawMessage `json:"tool_args,omitempty"`
276 ToolPreview string `json:"tool_preview,omitempty"`
277 Text string `json:"text,omitempty"`
278 Session *SessionContext `json:"session,omitempty"`
279 // State is private to the receiving extension. The manager selects the
280 // matching extension-owned snapshot before sending the event.
281 State json.RawMessage `json:"state,omitempty"`
282}
283
284type EventInterceptFromHost struct {
285 Type string `json:"type"`
286 ID string `json:"id"`
287 Event string `json:"event"`
288 ToolID string `json:"tool_id,omitempty"`
289 ToolName string `json:"tool_name,omitempty"`
290 ToolArgs json.RawMessage `json:"tool_args,omitempty"`
291 Step int `json:"step,omitempty"`
292 Text string `json:"text,omitempty"`
293}
294
295type PanelKeyFromHost struct {
296 Type string `json:"type"`
297 PanelID string `json:"panel_id"`
298 Key string `json:"key"`
299 Text string `json:"text,omitempty"`
300}
301
302type PanelResizeFromHost struct {
303 Type string `json:"type"`
304 PanelID string `json:"panel_id"`
305 Width int `json:"width"`
306 Height int `json:"height"`
307}
308
309type PanelCloseFromHost struct {
310 Type string `json:"type"`
311 PanelID string `json:"panel_id"`
312}
313
314type ShutdownFromHost struct {
315 Type string `json:"type"`
316}
317
318func Encode(v any) ([]byte, error) {
319 b, err := json.Marshal(v)
320 if err != nil {
321 return nil, err
322 }
323 return append(b, '\n'), nil
324}