| 💾 Saved. d722711 k33g 4h ago | 1 | // Package acp is bob's second front end: the same engine, the same tools and |
| 2 | // the same history rules as the terminal REPL (internal/agent), exposed over |
| 3 | // the Agent Client Protocol — JSON-RPC 2.0 on stdio, one message per line — |
| 4 | // so that an editor (Zed, IntelliJ, Neovim…) can host the agent. |
| 5 | // |
| 6 | // The division of labour: |
| 7 | // |
| 8 | // - the SDK (github.com/coder/acp-go-sdk) does the transport: framing, |
| 9 | // routing initialize/session/new/session/prompt to the methods below, |
| 10 | // correlating the ids of our own requests to the client (permissions); |
| 11 | // - this package translates: a prompt turn becomes one engine.Generate call, |
| 12 | // and the events the tools emit through ui.Sink become session/update |
| 13 | // notifications — text chunks, tool_call lifecycles, diffs; |
| 14 | // - nothing else changes: internal/engine and internal/tools are shared with |
| 15 | // the terminal front end, which is the point of the exercise. |
| 16 | // |
| 17 | // stdout carries JSON-RPC only. Everything human — the banner, the [acp] trail, |
| 18 | // the SDK's own diagnostics — goes to stderr, which the spec leaves to logging |
| 19 | // and which Zed shows in `dev: open acp logs`. |
| 20 | package acp |
| 21 | |
| 22 | import ( |
| 23 | "context" |
| 24 | "encoding/json" |
| 25 | "errors" |
| 26 | "fmt" |
| 27 | "io" |
| 28 | "log/slog" |
| 29 | "os" |
| 30 | "strings" |
| 31 | "sync" |
| 32 | |
| 33 | "mm/internal/compact" |
| 34 | "mm/internal/config" |
| 35 | "mm/internal/engine" |
| 36 | "mm/internal/mention" |
| 37 | history "mm/internal/session" |
| 38 | "mm/internal/ui" |
| 39 | |
| 40 | sdk "github.com/coder/acp-go-sdk" |
| 41 | "github.com/firebase/genkit/go/ai" |
| 42 | ) |
| 43 | |
| 44 | // version is what initialize reports in agentInfo — the part number, so a |
| 45 | // trace says which chapter of the talk produced it. |
| 46 | const version = "0.11.0" |
| 47 | |
| 48 | // session is one conversation with the client. Like the REPL, the history is |
| 49 | // plain []*ai.Message with the system prompt first; unlike the REPL there can |
| 50 | // be several of them, one per session/new. |
| 51 | type session struct { |
| 52 | cwd string |
| 53 | messages []*ai.Message |
| 54 | cancel context.CancelFunc // cancels the turn in flight, if any |
| 55 | // allowAlways remembers the tools the user granted for the whole session |
| 56 | // ("allow always" in the permission dialog). |
| 57 | allowAlways map[string]bool |
| 58 | } |
| 59 | |
| 60 | type front struct { |
| 61 | e *engine.Engine |
| 62 | system string |
| 63 | tools []ai.ToolRef |
| 64 | conn *sdk.AgentSideConnection |
| 65 | out *responseWriter // what the SDK writes to; see responseWriter |
| 66 | |
| 67 | mu sync.Mutex // guards sessions and nextID |
| 68 | sessions map[sdk.SessionId]*session |
| 69 | nextID int |
| 70 | |
| 71 | // turnMu serialises the turns. Two reasons, both structural: the active |
| 72 | // ui.Sink is process-global, and engine.Generate was never designed for two |
| 73 | // concurrent turns. The spec leaves this to the agent, and Zed does not |
| 74 | // send a second prompt while one is running — a queued turn just waits. |
| 75 | turnMu sync.Mutex |
| 76 | } |
| 77 | |
| 78 | var _ sdk.Agent = (*front)(nil) |
| 79 | |
| 80 | // Run wires the agent side of an ACP connection on stdio and blocks until the |
| 81 | // client closes it. Same arguments as agent.Run: main.go's wiring — engine, |
| 82 | // system prompt, tool list — is identical for the two front ends. |
| 83 | func Run(ctx context.Context, e *engine.Engine, system string, tools []ai.ToolRef) { |
| 84 | f := newFront(e, system, tools, os.Stdout, os.Stdin) |
| 85 | select { |
| 86 | case <-f.conn.Done(): // the editor closed our stdin: we are done |
| 87 | case <-ctx.Done(): |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // newFront wires a front end on an arbitrary pair of streams — stdio in Run, |
| 92 | // pipes in the tests. w is where WE write (JSON-RPC to the client), r where |
| 93 | // we read the client's messages. |
| 94 | func newFront(e *engine.Engine, system string, tools []ai.ToolRef, w io.Writer, r io.Reader) *front { |
| 95 | f := &front{e: e, system: system, tools: tools, sessions: map[sdk.SessionId]*session{}} |
| 96 | f.out = &responseWriter{Writer: w, hooks: map[sdk.SessionId]func(){}} |
| 97 | conn := sdk.NewAgentSideConnection(f, f.out, r) |
| 98 | conn.SetLogger(slog.New(slog.NewTextHandler(os.Stderr, nil))) |
| 99 | f.conn = conn |
| 100 | return f |
| 101 | } |
| 102 | |
| 103 | // responseWriter is the io.Writer handed to the SDK: the real output plus one |
| 104 | // hook per session, run once the session/new response carrying that session |
| 105 | // id has been written. It exists for available_commands_update. The spec says |
| 106 | // to announce the commands right after the session is created, and the client |
| 107 | // (Zed, verified 2026-09-15) drops any session/update for a session whose |
| 108 | // session/new response it has not received yet — "Available commands: none". |
| 109 | // But the SDK writes the response only when NewSession returns, after anything |
| 110 | // the method itself sent, and offers no "after the response" callback. Watching |
| 111 | // the bytes is the one place where the order is certain. |
| 112 | type responseWriter struct { |
| 113 | io.Writer |
| 114 | mu sync.Mutex |
| 115 | hooks map[sdk.SessionId]func() |
| 116 | } |
| 117 | |
| 118 | // afterResponse registers hook to run — once, in its own goroutine — after the |
| 119 | // response that carries sid has been written. |
| 120 | func (w *responseWriter) afterResponse(sid sdk.SessionId, hook func()) { |
| 121 | w.mu.Lock() |
| 122 | defer w.mu.Unlock() |
| 123 | w.hooks[sid] = hook |
| 124 | } |
| 125 | |
| 126 | // Write forwards to the real output, then fires the hook of the session whose |
| 127 | // response this line is, if any. The hook runs in a goroutine: the SDK holds |
| 128 | // its write lock here, and the hook is about to write a notification. |
| 129 | func (w *responseWriter) Write(p []byte) (int, error) { |
| 130 | n, err := w.Writer.Write(p) |
| 131 | if err != nil { |
| 132 | return n, err |
| 133 | } |
| 134 | if sid, ok := responseSessionID(p); ok { |
| 135 | w.mu.Lock() |
| 136 | hook := w.hooks[sid] |
| 137 | delete(w.hooks, sid) |
| 138 | w.mu.Unlock() |
| 139 | if hook != nil { |
| 140 | go hook() |
| 141 | } |
| 142 | } |
| 143 | return n, nil |
| 144 | } |
| 145 | |
| 146 | // responseSessionID recognises a JSON-RPC response whose result carries a |
| 147 | // sessionId — session/new (and session/load, which this agent does not |
| 148 | // offer). Notifications have no id and prompt responses no sessionId, so |
| 149 | // neither matches. |
| 150 | func responseSessionID(line []byte) (sdk.SessionId, bool) { |
| 151 | var m struct { |
| 152 | ID json.RawMessage `json:"id"` |
| 153 | Result struct { |
| 154 | SessionId sdk.SessionId `json:"sessionId"` |
| 155 | } `json:"result"` |
| 156 | } |
| 157 | if json.Unmarshal(line, &m) != nil || len(m.ID) == 0 || m.Result.SessionId == "" { |
| 158 | return "", false |
| 159 | } |
| 160 | return m.Result.SessionId, true |
| 161 | } |
| 162 | |
| 163 | // --- initialize --------------------------------------------------------------- |
| 164 | |
| 165 | func (f *front) Initialize(_ context.Context, p sdk.InitializeRequest) (sdk.InitializeResponse, error) { |
| 166 | fmt.Fprintf(os.Stderr, "[acp] initialize: client %q protocolVersion=%d\n", |
| 167 | clientName(p.ClientInfo), p.ProtocolVersion) |
| 168 | return sdk.InitializeResponse{ |
| 169 | ProtocolVersion: sdk.ProtocolVersionNumber, // 1 |
| 170 | // Everything absent is false: no loadSession, no image/audio prompts, |
| 171 | // no MCP over http. Honest capabilities are what lets a client adapt. |
| 172 | AgentCapabilities: sdk.AgentCapabilities{}, |
| 173 | AgentInfo: &sdk.Implementation{Name: "bob", Title: sdk.Ptr("Bob (bash-first agent)"), Version: version}, |
| 174 | AuthMethods: []sdk.AuthMethod{}, // the engine is local: nothing to log into |
| 175 | }, nil |
| 176 | } |
| 177 | |
| 178 | func clientName(impl *sdk.Implementation) string { |
| 179 | if impl == nil { |
| 180 | return "unknown" |
| 181 | } |
| 182 | return impl.Name |
| 183 | } |
| 184 | |
| 185 | func (f *front) Authenticate(context.Context, sdk.AuthenticateRequest) (sdk.AuthenticateResponse, error) { |
| 186 | return sdk.AuthenticateResponse{}, nil |
| 187 | } |
| 188 | |
| 189 | // --- session/new -------------------------------------------------------------- |
| 190 | |
| 191 | func (f *front) NewSession(ctx context.Context, p sdk.NewSessionRequest) (sdk.NewSessionResponse, error) { |
| 192 | f.mu.Lock() |
| 193 | f.nextID++ |
| 194 | sid := sdk.SessionId(fmt.Sprintf("bob-%d-%d", os.Getpid(), f.nextID)) |
| 195 | f.sessions[sid] = &session{ |
| 196 | cwd: p.Cwd, // absolute, per the spec; where bash runs for this session |
| 197 | messages: history.Fresh(f.system), |
| 198 | allowAlways: map[string]bool{}, |
| 199 | } |
| 200 | f.mu.Unlock() |
| 201 | // mcpServers is where the client offers extra tools. bob's whole point is |
| 202 | // the opposite — one built-in tool — so they are acknowledged and ignored. |
| 203 | fmt.Fprintf(os.Stderr, "[acp] session %s cwd=%s mcpServers=%d (ignored)\n", sid, p.Cwd, len(p.McpServers)) |
| 204 | // The slash commands, announced as the spec asks — right after the session |
| 205 | // is created — so the editor can list and complete them. "Right after" |
| 206 | // means after the response: Zed keys its sessions on the id that response |
| 207 | // carries and drops updates for an id it does not know yet. Hence the hook |
| 208 | // on the writer rather than a SessionUpdate call here (see responseWriter). |
| 209 | // The request context dies with this method; the hook needs one that lives. |
| 210 | f.out.afterResponse(sid, func() { |
| 211 | _ = f.conn.SessionUpdate(context.WithoutCancel(ctx), sdk.SessionNotification{ |
| 212 | SessionId: sid, |
| 213 | Update: sdk.SessionUpdate{AvailableCommandsUpdate: &sdk.SessionAvailableCommandsUpdate{AvailableCommands: availableCommands()}}, |
| 214 | }) |
| 215 | }) |
| 216 | return sdk.NewSessionResponse{SessionId: sid}, nil |
| 217 | } |
| 218 | |
| 219 | // compactCommand is the REPL's /compact, spelled here because ACP has no other |
| 220 | // place for it; /new is shared through internal/session. /quit and /abort are |
| 221 | // deliberately absent: the editor owns the process and ends it by closing |
| 222 | // stdin, and it cancels a turn with its own stop key through session/cancel. |
| 223 | const compactCommand = "/compact" |
| 224 | |
| 225 | // isCommand says whether input, trimmed, is exactly the given command — the |
| 226 | // same rule as session.IsNewCommand: "/compact " is the command, "/compact |
| 227 | // now" is a question for the model. |
| 228 | func isCommand(input, command string) bool { |
| 229 | return strings.TrimSpace(input) == command |
| 230 | } |
| 231 | |
| 232 | // availableCommands lists the slash commands this front end handles itself, |
| 233 | // in the shape ACP advertises them: bare names, the client adds the slash. |
| 234 | // Only what session/prompt actually intercepts belongs here — announcing |
| 235 | // /quit or /abort, which the REPL has and this front end does not, would |
| 236 | // promise the editor something the next prompt would hand to the model. |
| 237 | func availableCommands() []sdk.AvailableCommand { |
| 238 | return []sdk.AvailableCommand{ |
| 239 | {Name: history.NewCommandName, Description: "Clear the history and start a new session"}, |
| 240 | {Name: strings.TrimPrefix(compactCommand, "/"), Description: "Compress the history now, keeping the last turns"}, |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // --- session/prompt ----------------------------------------------------------- |
| 245 | |
| 246 | func (f *front) Prompt(ctx context.Context, p sdk.PromptRequest) (sdk.PromptResponse, error) { |
| 247 | f.mu.Lock() |
| 248 | s, ok := f.sessions[p.SessionId] |
| 249 | f.mu.Unlock() |
| 250 | if !ok { |
| 251 | return sdk.PromptResponse{}, sdk.NewInvalidParams(map[string]any{"sessionId": p.SessionId}) |
| 252 | } |
| 253 | |
| 254 | input := flatten(p.Prompt) |
| 255 | |
| 256 | f.turnMu.Lock() |
| 257 | defer f.turnMu.Unlock() |
| 258 | |
| 259 | // A slash command is answered here, without a turn: it is the REPL's "only |
| 260 | // between two generations" rule, met for free — turnMu guarantees no |
| 261 | // generation is running on this session's history while we replace it. |
| 262 | switch { |
| 263 | case history.IsNewCommand(input): |
| 264 | return f.startNewSession(ctx, p.SessionId, s), nil |
| 265 | case isCommand(input, compactCommand): |
| 266 | return f.compactSession(ctx, p.SessionId, s), nil |
| 267 | } |
| 268 | |
| 269 | // One cancellable context per turn: session/cancel fires s.cancel, the |
| 270 | // generation and any pending permission request fall with it, and the spec's |
| 271 | // contract — still answer the prompt, with stopReason "cancelled" — is met |
| 272 | // at the bottom of this function. |
| 273 | turnCtx, cancel := context.WithCancel(ctx) |
| 274 | f.mu.Lock() |
| 275 | s.cancel = cancel |
| 276 | f.mu.Unlock() |
| 277 | defer func() { |
| 278 | f.mu.Lock() |
| 279 | s.cancel = nil |
| 280 | f.mu.Unlock() |
| 281 | cancel() |
| 282 | }() |
| 283 | |
| 284 | // The sink is what turns the tools' events into session/update messages; |
| 285 | // installing it is what switches engine+tools to "ACP mode" for this turn. |
| 286 | ui.SetActive(&sink{f: f, ctx: turnCtx, sid: p.SessionId, s: s}) |
| 287 | defer ui.SetActive(nil) |
| 288 | |
| 289 | // A typed "@path" — the editor's picker bypassed, or an editor without |
| 290 | // one — is understood like the resource_link the picker would have sent, |
| 291 | // against the session's cwd, which is the project the editor opened. |
| 292 | input, attached := mention.Expand(input, s.cwd) |
| 293 | for _, a := range attached { |
| 294 | fmt.Fprintf(os.Stderr, "[acp] session %s: attached %s\n", p.SessionId, a.Path) |
| 295 | } |
| 296 | s.messages = append(s.messages, ai.NewUserTextMessage(input)) |
| 297 | before := len(s.messages) |
| 298 | |
| 299 | resp, history, err := f.e.Generate(turnCtx, s.messages, f.tools) |
| 300 | |
| 301 | // Same rule as the REPL, for the same reason: keep the FULL history even |
| 302 | // when the turn failed or was cancelled — the commands that already ran are |
| 303 | // part of the conversation, and forgetting them makes the model re-run or |
| 304 | // invent them on the next turn. |
| 305 | switch { |
| 306 | case len(history) > before: |
| 307 | s.messages = history |
| 308 | case resp != nil && resp.Message != nil: |
| 309 | s.messages = append(s.messages, resp.Message) |
| 310 | case err != nil: |
| 311 | s.messages = s.messages[:before-1] // question without an answer: forget it |
| 312 | } |
| 313 | |
| 314 | // The ⚙ recap of the REPL, one line on stderr: the editor already renders |
| 315 | // every tool call, but the trail is what lets `dev: open acp logs` say at a |
| 316 | // glance whether the model worked or improvised. |
| 317 | turn := s.messages[min(before, len(s.messages)):] |
| 318 | fmt.Fprintf(os.Stderr, "[acp] turn done: %d command(s) · %d file op(s) · %d skill(s)\n", |
| 319 | len(engine.CommandList(turn)), len(engine.FileOps(turn)), engine.Skills(turn)) |
| 320 | |
| 321 | switch { |
| 322 | case turnCtx.Err() != nil: |
| 323 | // The turn was cancelled — and both roads lead here: our Cancel method |
| 324 | // fires s.cancel (the child), AND the SDK cancels the request's own |
| 325 | // context when session/cancel arrives (the parent — measured: with the |
| 326 | // parent-must-be-intact test the REPL uses, a cancelled turn answered |
| 327 | // "Internal error: stream error: context canceled" instead of honouring |
| 328 | // the spec's contract). Either way the answer is the same: respond to |
| 329 | // session/prompt anyway, with stopReason "cancelled". |
| 330 | return sdk.PromptResponse{StopReason: sdk.StopReasonCancelled}, nil |
| 331 | case err != nil: |
| 332 | // One line, in the provider's words — the same text the REPL prints |
| 333 | // between brackets, delivered as a JSON-RPC error. |
| 334 | return sdk.PromptResponse{}, sdk.NewInternalError(map[string]any{"error": f.e.Explain(err)}) |
| 335 | } |
| 336 | return sdk.PromptResponse{StopReason: sdk.StopReasonEndTurn}, nil |
| 337 | } |
| 338 | |
| 339 | // startNewSession is the ACP side of /new: the session keeps its id, its cwd |
| 340 | // and the "allow always" grants — the dialog promised them "for this session", |
| 341 | // and the session, as the editor sees it, is the same one — but its history |
| 342 | // goes back to the system prompt alone, and the server's token count of the |
| 343 | // last context is dropped with it, as after a compression. The one line the |
| 344 | // REPL prints travels as an agent message so the editor shows it in the |
| 345 | // thread, and the turn ends right there: the model is not consulted. |
| 346 | func (f *front) startNewSession(ctx context.Context, sid sdk.SessionId, s *session) sdk.PromptResponse { |
| 347 | forgotten := resetSession(s, f.system) |
| 348 | f.e.ForgetInputTokens() |
| 349 | fmt.Fprintf(os.Stderr, "[acp] session %s: new session, %d message(s) forgotten\n", sid, forgotten) |
| 350 | _ = f.conn.SessionUpdate(ctx, sdk.SessionNotification{ |
| 351 | SessionId: sid, |
| 352 | Update: sdk.UpdateAgentMessageText(fmt.Sprintf("🆕 New session: %d message(s) forgotten.", forgotten)), |
| 353 | }) |
| 354 | return sdk.PromptResponse{StopReason: sdk.StopReasonEndTurn} |
| 355 | } |
| 356 | |
| 357 | // compactSession is the ACP side of /compact: the REPL's compactHistory with |
| 358 | // the report sent as an agent message instead of printed. Forced, like the |
| 359 | // REPL's — the threshold is for the automatic path, which this front end does |
| 360 | // not have — so "nothing to compact" is said rather than swallowed. Runs under |
| 361 | // turnMu: Genkit is not holding this history. |
| 362 | func (f *front) compactSession(ctx context.Context, sid sdk.SessionId, s *session) sdk.PromptResponse { |
| 363 | cfg := config.Cfg.Context |
| 364 | line, compressed := compactHistory(ctx, s, cfg, func(ctx context.Context, request []*ai.Message) (string, error) { |
| 365 | return f.e.Summarize(ctx, request, cfg.SummaryMaxTokens) |
| 366 | }, f.e.Explain) |
| 367 | if compressed { |
| 368 | f.e.ForgetInputTokens() // the server's measure described the history just replaced |
| 369 | } |
| 370 | fmt.Fprintf(os.Stderr, "[acp] session %s: /compact — %s\n", sid, line) |
| 371 | _ = f.conn.SessionUpdate(ctx, sdk.SessionNotification{ |
| 372 | SessionId: sid, |
| 373 | Update: sdk.UpdateAgentMessageText(line), |
| 374 | }) |
| 375 | return sdk.PromptResponse{StopReason: sdk.StopReasonEndTurn} |
| 376 | } |
| 377 | |
| 378 | // compactHistory compresses the session's history in place when there is |
| 379 | // something to compress, and returns the one line to show — the REPL's words, |
| 380 | // so the editor and the terminal agree — and whether the history changed. |
| 381 | // Kept apart from compactSession, with the summariser and the error explainer |
| 382 | // injected, so the three outcomes are testable without an engine or a |
| 383 | // connection. |
| 384 | func compactHistory(ctx context.Context, s *session, cfg config.ContextConfig, summarize compact.Summarizer, explain func(error) string) (line string, compressed bool) { |
| 385 | res, err := compact.Compact(ctx, s.messages, cfg, summarize) |
| 386 | switch { |
| 387 | case errors.Is(err, compact.ErrNothingToCompact): |
| 388 | return fmt.Sprintf("🗜️ nothing to compact: %d message(s), no turn older than the last %d", len(s.messages), cfg.KeepLastTurns), false |
| 389 | case err != nil: |
| 390 | return fmt.Sprintf("[compact: failed, history kept: %s]", explain(err)), false |
| 391 | } |
| 392 | s.messages = res.Messages |
| 393 | return "🗜️ " + res.Report(), true |
| 394 | } |
| 395 | |
| 396 | // resetSession replaces the session's history with a fresh one and returns how |
| 397 | // many messages were forgotten. Kept apart from startNewSession so the rule |
| 398 | // — history reset, permissions kept — is testable without a connection. |
| 399 | func resetSession(s *session, system string) int { |
| 400 | forgotten := history.Forgotten(s.messages) |
| 401 | s.messages = history.Fresh(system) |
| 402 | return forgotten |
| 403 | } |
| 404 | |
| 405 | // flatten turns the prompt's content blocks into the plain text bob reads. |
| 406 | // Text is taken as-is; a resource_link (a file the user @-mentioned through |
| 407 | // the editor's picker) becomes its path, in the same shape mention.Expand |
| 408 | // gives a typed @path — the model has tools to read it, that is the whole |
| 409 | // idea; anything else (image, audio, embedded resource) was not announced in |
| 410 | // our capabilities, so a compliant client never sends it. |
| 411 | func flatten(blocks []sdk.ContentBlock) string { |
| 412 | var b strings.Builder |
| 413 | for _, c := range blocks { |
| 414 | switch { |
| 415 | case c.Text != nil: |
| 416 | b.WriteString(c.Text.Text) |
| 417 | case c.ResourceLink != nil: |
| 418 | fmt.Fprintf(&b, "\n[attached file: %s]", strings.TrimPrefix(c.ResourceLink.Uri, "file://")) |
| 419 | } |
| 420 | } |
| 421 | return b.String() |
| 422 | } |
| 423 | |
| 424 | // --- session/cancel ----------------------------------------------------------- |
| 425 | |
| 426 | func (f *front) Cancel(_ context.Context, p sdk.CancelNotification) error { |
| 427 | f.mu.Lock() |
| 428 | defer f.mu.Unlock() |
| 429 | if s, ok := f.sessions[p.SessionId]; ok && s.cancel != nil { |
| 430 | s.cancel() |
| 431 | } |
| 432 | return nil |
| 433 | } |
| 434 | |
| 435 | // --- the rest of sdk.Agent: not supported, and saying so ----------------------- |
| 436 | // |
| 437 | // The SDK requires the full interface; answering "method not found" is the |
| 438 | // protocol's way of saying a capability we never announced is indeed absent. |
| 439 | |
| 440 | func (f *front) SetSessionMode(context.Context, sdk.SetSessionModeRequest) (sdk.SetSessionModeResponse, error) { |
| 441 | return sdk.SetSessionModeResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodSessionSetMode) |
| 442 | } |
| 443 | |
| 444 | func (f *front) SetSessionConfigOption(context.Context, sdk.SetSessionConfigOptionRequest) (sdk.SetSessionConfigOptionResponse, error) { |
| 445 | return sdk.SetSessionConfigOptionResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodSessionSetConfigOption) |
| 446 | } |
| 447 | |
| 448 | func (f *front) ListSessions(context.Context, sdk.ListSessionsRequest) (sdk.ListSessionsResponse, error) { |
| 449 | return sdk.ListSessionsResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodSessionList) |
| 450 | } |
| 451 | |
| 452 | func (f *front) ResumeSession(context.Context, sdk.ResumeSessionRequest) (sdk.ResumeSessionResponse, error) { |
| 453 | return sdk.ResumeSessionResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodSessionResume) |
| 454 | } |
| 455 | |
| 456 | func (f *front) CloseSession(context.Context, sdk.CloseSessionRequest) (sdk.CloseSessionResponse, error) { |
| 457 | return sdk.CloseSessionResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodSessionClose) |
| 458 | } |
| 459 | |
| 460 | func (f *front) Logout(context.Context, sdk.LogoutRequest) (sdk.LogoutResponse, error) { |
| 461 | return sdk.LogoutResponse{}, sdk.NewMethodNotFound(sdk.AgentMethodLogout) |
| 462 | } |