-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconnection.go
More file actions
407 lines (368 loc) · 12.8 KB
/
Copy pathconnection.go
File metadata and controls
407 lines (368 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
package acpruntime
import (
"context"
"encoding/json"
"os"
"strings"
"sync"
)
type Connection struct {
peer *Peer
observability ObservabilityOptions
permissionObserverMu sync.RWMutex
permissionObserver func(PermissionRequest, PermissionDecision)
}
// SetPermissionObserver registers a callback invoked whenever the agent sends
// a session/request_permission request. The host's decision is passed through
// unchanged so the runtime can record the request in its read model without
// affecting the outcome returned to the agent.
func (c *Connection) SetPermissionObserver(handler func(PermissionRequest, PermissionDecision)) {
c.permissionObserverMu.Lock()
defer c.permissionObserverMu.Unlock()
c.permissionObserver = handler
}
type ConnectionHandle struct {
Connection *Connection
Dispose func(context.Context) error
}
type ConnectionFactoryInput struct {
Agent Agent
Client Client
CWD string
Observability ObservabilityOptions
Authority AuthorityHandlers
}
type ConnectionFactory func(context.Context, ConnectionFactoryInput) (ConnectionHandle, error)
type Client struct {
Info Implementation
Capabilities ClientCapabilities
Authority AuthorityHandlers
}
func NewConnection(peer *Peer, client Client) *Connection {
return NewConnectionWithObservability(peer, client, ObservabilityOptions{})
}
func NewConnectionWithObservability(peer *Peer, client Client, observability ObservabilityOptions) *Connection {
conn := &Connection{peer: peer, observability: observability}
if client.Authority.Permission != nil {
peer.RegisterRequest("session/request_permission", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req struct {
SessionID string `json:"sessionId"`
ToolCallID string `json:"toolCallId"`
Title string `json:"title"`
Kind string `json:"kind"`
Options []PermissionOption `json:"options"`
}
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
permissionReq := PermissionRequest{
SessionID: req.SessionID,
ToolCallID: req.ToolCallID,
Title: req.Title,
Kind: req.Kind,
Options: req.Options,
}
decision, err := client.Authority.Permission(ctx, permissionReq)
if err != nil {
return nil, err
}
conn.permissionObserverMu.RLock()
observer := conn.permissionObserver
conn.permissionObserverMu.RUnlock()
if observer != nil {
observer(permissionReq, decision)
}
return permissionResponse{Outcome: decision.Outcome, OptionID: decision.OptionID}, nil
})
}
if client.Authority.Filesystem != nil {
peer.RegisterRequest("fs/read_text_file", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req struct {
Path string `json:"path"`
}
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
text, err := client.Authority.Filesystem.ReadTextFile(ctx, req.Path)
if err != nil {
return nil, err
}
return readTextFileResponse{Content: text}, nil
})
peer.RegisterRequest("fs/write_text_file", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req struct {
Path string `json:"path"`
Content string `json:"content"`
}
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
return emptyResponse{}, client.Authority.Filesystem.WriteTextFile(ctx, req.Path, req.Content)
})
}
if client.Authority.Terminal != nil {
registerTerminalHandlers(peer, client.Authority.Terminal)
}
return conn
}
// registerTerminalHandlers wires the five ACP terminal methods
// (terminal/create, terminal/output, terminal/wait_for_exit, terminal/kill,
// terminal/release) to the host-supplied TerminalHandler. These methods are
// invoked by the agent and implemented by the host. See
// https://agentclientprotocol.com/protocol/v1/terminals for the wire format.
//
// Note the protocol asymmetry: terminal/output nests the exit status under
// "exitStatus", while terminal/wait_for_exit inlines exitCode/signal at the
// top level of the result object.
func registerTerminalHandlers(peer *Peer, terminal TerminalHandler) {
peer.RegisterRequest("terminal/create", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req struct {
SessionID string `json:"sessionId"`
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env []EnvVariable `json:"env,omitempty"`
CWD *string `json:"cwd,omitempty"`
OutputByteLimit *uint64 `json:"outputByteLimit,omitempty"`
}
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
var cwd string
if req.CWD != nil {
cwd = *req.CWD
}
result, err := terminal.CreateTerminal(ctx, CreateTerminalRequest{
SessionID: req.SessionID,
Command: req.Command,
Args: req.Args,
Env: req.Env,
CWD: cwd,
OutputByteLimit: req.OutputByteLimit,
})
if err != nil {
return nil, err
}
return createTerminalResponse{TerminalID: result.TerminalID}, nil
})
peer.RegisterRequest("terminal/output", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req terminalIDRequest
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
result, err := terminal.Output(ctx, req.TerminalID)
if err != nil {
return nil, err
}
return terminalOutputResponse{Output: result.Output, Truncated: result.Truncated, ExitStatus: toWireExitStatus(result.ExitStatus)}, nil
})
peer.RegisterRequest("terminal/wait_for_exit", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req terminalIDRequest
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
status, err := terminal.WaitForExit(ctx, req.TerminalID)
if err != nil {
return nil, err
}
return waitForTerminalExitResponse{ExitCode: status.ExitCode, Signal: status.Signal}, nil
})
peer.RegisterRequest("terminal/kill", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req terminalIDRequest
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
if err := terminal.Kill(ctx, req.TerminalID); err != nil {
return nil, err
}
return emptyResponse{}, nil
})
peer.RegisterRequest("terminal/release", func(ctx context.Context, raw json.RawMessage) (any, error) {
var req terminalIDRequest
if err := json.Unmarshal(raw, &req); err != nil {
return nil, err
}
if err := terminal.Release(ctx, req.TerminalID); err != nil {
return nil, err
}
return emptyResponse{}, nil
})
}
type terminalIDRequest struct {
SessionID string `json:"sessionId"`
TerminalID string `json:"terminalId"`
}
func toWireExitStatus(status *TerminalExitStatus) *terminalExitStatusJSON {
if status == nil {
return nil
}
return &terminalExitStatusJSON{ExitCode: status.ExitCode, Signal: status.Signal}
}
func (c *Connection) SetSessionUpdateHandler(handler func(context.Context, SessionNotification)) {
c.peer.RegisterNotification("session/update", func(ctx context.Context, raw json.RawMessage) {
var notification SessionNotification
if err := json.Unmarshal(raw, ¬ification); err != nil {
c.emitProtocolError(ctx, "session/update", raw, err)
return
}
handler(ctx, notification)
})
}
func (c *Connection) Initialize(ctx context.Context, req InitializeRequest) (InitializeResponse, error) {
var resp InitializeResponse
err := c.peer.Call(ctx, "initialize", req, &resp)
return resp, err
}
func (c *Connection) Authenticate(ctx context.Context, req AuthenticateRequest) (AuthenticateResponse, error) {
var resp AuthenticateResponse
err := c.peer.Call(ctx, "authenticate", req, &resp)
return resp, err
}
func (c *Connection) NewSession(ctx context.Context, req NewSessionRequest) (NewSessionResponse, error) {
var resp NewSessionResponse
err := c.peer.Call(ctx, "session/new", req, &resp)
return resp, err
}
func (c *Connection) LoadSession(ctx context.Context, req LoadSessionRequest) (LoadSessionResponse, error) {
var resp LoadSessionResponse
err := c.peer.Call(ctx, "session/load", req, &resp)
return resp, err
}
func (c *Connection) ResumeSession(ctx context.Context, req ResumeSessionRequest) (ResumeSessionResponse, error) {
var resp ResumeSessionResponse
err := c.peer.Call(ctx, "session/resume", req, &resp)
return resp, err
}
func (c *Connection) ForkSession(ctx context.Context, req ForkSessionRequest) (ForkSessionResponse, error) {
var resp ForkSessionResponse
err := c.peer.Call(ctx, "session/fork", req, &resp)
return resp, err
}
func (c *Connection) ListSessions(ctx context.Context, req ListSessionsRequest) (ListSessionsResponse, error) {
var resp ListSessionsResponse
err := c.peer.Call(ctx, "session/list", req, &resp)
return resp, err
}
func (c *Connection) Prompt(ctx context.Context, req PromptRequest) (PromptResponse, error) {
var resp PromptResponse
err := c.peer.Call(ctx, "session/prompt", req, &resp)
return resp, err
}
func (c *Connection) Cancel(ctx context.Context, req CancelRequest) error {
return c.peer.Notify(ctx, "session/cancel", req)
}
func (c *Connection) SetSessionMode(ctx context.Context, req SetSessionModeRequest) error {
var resp SetSessionModeResponse
return c.peer.Call(ctx, "session/set_mode", req, &resp)
}
func (c *Connection) SetSessionConfigOption(ctx context.Context, req SetSessionConfigOptionRequest) (SetSessionConfigOptionResponse, error) {
var resp SetSessionConfigOptionResponse
err := c.peer.Call(ctx, "session/set_config_option", req, &resp)
return resp, err
}
func (c *Connection) CloseSession(ctx context.Context, req CloseSessionRequest) error {
var resp CloseSessionResponse
return c.peer.Call(ctx, "session/close", req, &resp)
}
// DeleteSession deletes a session's persistent history (session/delete). Unlike
// CloseSession, this removes the session from the agent's storage entirely.
func (c *Connection) DeleteSession(ctx context.Context, req DeleteSessionRequest) error {
var resp DeleteSessionResponse
return c.peer.Call(ctx, "session/delete", req, &resp)
}
// Logout asks the agent to discard cached credentials (logout).
func (c *Connection) Logout(ctx context.Context, req LogoutRequest) error {
var resp LogoutResponse
return c.peer.Call(ctx, "logout", req, &resp)
}
func defaultClient(options RuntimeOptions, handlers AuthorityHandlers) Client {
info := options.ClientInfo
if info.Name == "" {
info = Implementation{Name: "acp-runtime-go", Version: "0.1.0"}
}
if handlers.Permission == nil {
handlers.Permission = options.AuthorityHandlers.Permission
}
if handlers.Filesystem == nil {
handlers.Filesystem = options.AuthorityHandlers.Filesystem
}
if handlers.Terminal == nil {
handlers.Terminal = options.AuthorityHandlers.Terminal
}
return Client{
Info: info,
Capabilities: ClientCapabilities{
Terminal: handlers.Terminal != nil,
FS: FilesystemCapabilities{
ReadTextFile: handlers.Filesystem != nil,
WriteTextFile: handlers.Filesystem != nil,
},
},
Authority: handlers,
}
}
func envSlice(env map[string]string) []string {
if len(env) == 0 {
return os.Environ()
}
merged := map[string]string{}
for _, item := range os.Environ() {
for i := 0; i < len(item); i++ {
if item[i] == '=' {
merged[item[:i]] = item[i+1:]
break
}
}
}
for key, value := range env {
merged[key] = value
}
out := make([]string, 0, len(merged))
for key, value := range merged {
out = append(out, key+"="+value)
}
return out
}
type permissionResponse struct {
Outcome string `json:"outcome"`
OptionID string `json:"optionId,omitempty"`
}
type readTextFileResponse struct {
Content string `json:"content"`
}
type emptyResponse struct{}
type createTerminalResponse struct {
TerminalID string `json:"terminalId"`
}
type terminalOutputResponse struct {
Output string `json:"output"`
Truncated bool `json:"truncated"`
ExitStatus *terminalExitStatusJSON `json:"exitStatus,omitempty"`
}
// waitForTerminalExitResponse inlines exitCode/signal at the top level per
// the ACP v1 schema (asymmetric with terminal/output, which nests them).
type waitForTerminalExitResponse struct {
ExitCode *uint32 `json:"exitCode,omitempty"`
Signal *string `json:"signal,omitempty"`
}
type terminalExitStatusJSON struct {
ExitCode *uint32 `json:"exitCode,omitempty"`
Signal *string `json:"signal,omitempty"`
}
func (c *Connection) emitProtocolError(ctx context.Context, method string, raw json.RawMessage, err error) {
if c.observability.OnProtocolError == nil {
return
}
event := ProtocolErrorEvent{Method: method, Err: err}
if shouldCaptureProtocolErrorRaw(c.observability.CaptureContent) {
event.Raw = copyRawMessage(raw)
}
c.observability.OnProtocolError(ctx, event)
}
func shouldCaptureProtocolErrorRaw(mode string) bool {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "all", "full", "raw":
return true
default:
return false
}
}