Skip to content

Commit d854936

Browse files
author
Test
committed
fix(agents): critical model selection bug — approved_models schema mismatch
- setup.yaml had approved_models nested under defaults (wrong), moved to top-level with for_actions (correct Go struct schema) - ModelSelector now REFUSES to fall through to scored catalog when approved models are configured — prevents garbage/spam models like 'Hunter Alpha' - Added initial ACP protocol layer (internal/acp/)
1 parent 13e17a0 commit d854936

File tree

6 files changed

+1332
-2
lines changed

6 files changed

+1332
-2
lines changed

.github/workflows/cd.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ jobs:
150150
git tag -a "$NEW_TAG" -m "Automated release $NEW_TAG"
151151
git push origin "$NEW_TAG"
152152
153+
- name: Build binary for E2E tests
154+
run: go build -o $(go env GOPATH)/bin/gptcode ./cmd/gptcode
155+
153156
- name: Run tests
154157
run: go test -v ./...
155158

internal/acp/protocol.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// Package acp implements the Agent Client Protocol (ACP) for GPTCode CLI.
2+
//
3+
// ACP is a JSON-RPC 2.0 based protocol that standardizes communication between
4+
// code editors/IDEs and AI coding agents. It is analogous to LSP but for AI agents.
5+
//
6+
// Transport: stdio (newline-delimited JSON-RPC messages)
7+
// Spec: https://agentclientprotocol.com
8+
package acp
9+
10+
import (
11+
"encoding/json"
12+
"fmt"
13+
)
14+
15+
// ProtocolVersion is the ACP protocol version we support.
16+
const ProtocolVersion = 1
17+
18+
// --- JSON-RPC 2.0 types ---
19+
20+
// Request represents a JSON-RPC 2.0 request from client or agent.
21+
type Request struct {
22+
JSONRPC string `json:"jsonrpc"`
23+
ID interface{} `json:"id"`
24+
Method string `json:"method"`
25+
Params json.RawMessage `json:"params,omitempty"`
26+
}
27+
28+
// Response represents a JSON-RPC 2.0 response.
29+
type Response struct {
30+
JSONRPC string `json:"jsonrpc"`
31+
ID interface{} `json:"id"`
32+
Result interface{} `json:"result,omitempty"`
33+
Error *RPCError `json:"error,omitempty"`
34+
}
35+
36+
// Notification represents a JSON-RPC 2.0 notification (no ID, no response expected).
37+
type Notification struct {
38+
JSONRPC string `json:"jsonrpc"`
39+
Method string `json:"method"`
40+
Params interface{} `json:"params,omitempty"`
41+
}
42+
43+
// RPCError represents a JSON-RPC 2.0 error object.
44+
type RPCError struct {
45+
Code int `json:"code"`
46+
Message string `json:"message"`
47+
Data interface{} `json:"data,omitempty"`
48+
}
49+
50+
func (e *RPCError) Error() string {
51+
return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
52+
}
53+
54+
// Standard JSON-RPC 2.0 error codes.
55+
const (
56+
CodeParseError = -32700
57+
CodeInvalidRequest = -32600
58+
CodeMethodNotFound = -32601
59+
CodeInvalidParams = -32602
60+
CodeInternalError = -32603
61+
)
62+
63+
// --- ACP Method constants ---
64+
65+
const (
66+
// Agent baseline methods (client → agent)
67+
MethodInitialize = "initialize"
68+
MethodAuthenticate = "authenticate"
69+
MethodSessionNew = "session/new"
70+
MethodSessionPrompt = "session/prompt"
71+
72+
// Agent optional methods
73+
MethodSessionLoad = "session/load"
74+
MethodSessionSetMode = "session/set_mode"
75+
76+
// Agent notifications (client → agent)
77+
MethodSessionCancel = "session/cancel"
78+
79+
// Client baseline methods (agent → client)
80+
MethodRequestPermission = "session/request_permission"
81+
82+
// Client optional methods (agent → client)
83+
MethodFSReadTextFile = "fs/read_text_file"
84+
MethodFSWriteTextFile = "fs/write_text_file"
85+
MethodTerminalCreate = "terminal/create"
86+
MethodTerminalOutput = "terminal/output"
87+
MethodTerminalRelease = "terminal/release"
88+
MethodTerminalWaitExit = "terminal/wait_for_exit"
89+
MethodTerminalKill = "terminal/kill"
90+
91+
// Client notifications (agent → client)
92+
MethodSessionUpdate = "session/update"
93+
)
94+
95+
// --- Initialization types ---
96+
97+
// InitializeParams is sent by the client in the initialize request.
98+
type InitializeParams struct {
99+
ProtocolVersion int `json:"protocolVersion"`
100+
ClientCapabilities ClientCapabilities `json:"clientCapabilities"`
101+
ClientInfo ImplementationInfo `json:"clientInfo"`
102+
}
103+
104+
// InitializeResult is the agent's response to initialize.
105+
type InitializeResult struct {
106+
ProtocolVersion int `json:"protocolVersion"`
107+
AgentCapabilities AgentCapabilities `json:"agentCapabilities"`
108+
AgentInfo ImplementationInfo `json:"agentInfo"`
109+
AuthMethods []interface{} `json:"authMethods"`
110+
}
111+
112+
// ClientCapabilities describes what the client can do.
113+
type ClientCapabilities struct {
114+
FS *FSCapabilities `json:"fs,omitempty"`
115+
Terminal bool `json:"terminal,omitempty"`
116+
}
117+
118+
// FSCapabilities describes file system capabilities of the client.
119+
type FSCapabilities struct {
120+
ReadTextFile bool `json:"readTextFile,omitempty"`
121+
WriteTextFile bool `json:"writeTextFile,omitempty"`
122+
}
123+
124+
// AgentCapabilities describes what the agent supports.
125+
type AgentCapabilities struct {
126+
LoadSession bool `json:"loadSession"`
127+
PromptCapabilities PromptCapabilities `json:"promptCapabilities"`
128+
MCPCapabilities MCPCapabilities `json:"mcpCapabilities"`
129+
}
130+
131+
// PromptCapabilities describes what content types the agent accepts.
132+
type PromptCapabilities struct {
133+
Image bool `json:"image"`
134+
Audio bool `json:"audio"`
135+
EmbeddedContext bool `json:"embeddedContext"`
136+
}
137+
138+
// MCPCapabilities describes MCP support.
139+
type MCPCapabilities struct {
140+
HTTP bool `json:"http"`
141+
SSE bool `json:"sse"`
142+
}
143+
144+
// ImplementationInfo identifies an agent or client implementation.
145+
type ImplementationInfo struct {
146+
Name string `json:"name"`
147+
Title string `json:"title"`
148+
Version string `json:"version"`
149+
}
150+
151+
// --- Session types ---
152+
153+
// SessionNewParams is the params for session/new.
154+
type SessionNewParams struct {
155+
WorkingDirectory string `json:"workingDirectory,omitempty"`
156+
ConfigOptions map[string]string `json:"configOptions,omitempty"`
157+
}
158+
159+
// SessionNewResult is the result of session/new.
160+
type SessionNewResult struct {
161+
SessionID string `json:"sessionId"`
162+
}
163+
164+
// SessionPromptParams is the params for session/prompt.
165+
type SessionPromptParams struct {
166+
SessionID string `json:"sessionId"`
167+
Content []ContentBlock `json:"content"`
168+
}
169+
170+
// ContentBlock represents a piece of content in a prompt or update.
171+
type ContentBlock struct {
172+
Type string `json:"type"` // "text", "image", "resource", "resourceLink"
173+
Text string `json:"text,omitempty"`
174+
}
175+
176+
// SessionPromptResult is the result of session/prompt (returned when the turn ends).
177+
type SessionPromptResult struct {
178+
StopReason string `json:"stopReason"` // "endTurn", "cancelled", "maxTurns"
179+
}
180+
181+
// --- Update notification types ---
182+
183+
// SessionUpdateParams is sent as a notification from agent to client during a turn.
184+
type SessionUpdateParams struct {
185+
SessionID string `json:"sessionId"`
186+
Update SessionUpdate `json:"update"`
187+
}
188+
189+
// SessionUpdate is the polymorphic update payload.
190+
type SessionUpdate struct {
191+
Kind string `json:"kind"` // "message", "toolCall", "toolCallUpdate", "plan", "availableCommands", "modeChange"
192+
193+
// For kind="message"
194+
MessageChunk *MessageChunk `json:"messageChunk,omitempty"`
195+
196+
// For kind="toolCall"
197+
ToolCall *ToolCallUpdate `json:"toolCall,omitempty"`
198+
199+
// For kind="toolCallUpdate"
200+
ToolCallDelta *ToolCallDelta `json:"toolCallDelta,omitempty"`
201+
202+
// For kind="plan"
203+
Plan *PlanUpdate `json:"plan,omitempty"`
204+
205+
// For kind="availableCommands"
206+
Commands []SlashCommand `json:"commands,omitempty"`
207+
}
208+
209+
// MessageChunk is a text chunk streamed to the client.
210+
type MessageChunk struct {
211+
Content []ContentBlock `json:"content"`
212+
}
213+
214+
// ToolCallUpdate notifies the client about a tool invocation.
215+
type ToolCallUpdate struct {
216+
ToolCallID string `json:"toolCallId"`
217+
ToolName string `json:"toolName"`
218+
Status string `json:"status"` // "running", "completed", "error"
219+
Input string `json:"input,omitempty"`
220+
Output string `json:"output,omitempty"`
221+
}
222+
223+
// ToolCallDelta is a partial update for a long-running tool call.
224+
type ToolCallDelta struct {
225+
ToolCallID string `json:"toolCallId"`
226+
Output string `json:"output,omitempty"`
227+
}
228+
229+
// PlanUpdate provides plan information to the client.
230+
type PlanUpdate struct {
231+
Title string `json:"title"`
232+
Steps []PlanStep `json:"steps"`
233+
}
234+
235+
// PlanStep is a single step in an agent plan.
236+
type PlanStep struct {
237+
Title string `json:"title"`
238+
Status string `json:"status"` // "pending", "running", "completed", "error"
239+
}
240+
241+
// SlashCommand describes an available slash command.
242+
type SlashCommand struct {
243+
Name string `json:"name"`
244+
Description string `json:"description"`
245+
}
246+
247+
// --- Permission types ---
248+
249+
// RequestPermissionParams is sent from agent to client.
250+
type RequestPermissionParams struct {
251+
SessionID string `json:"sessionId"`
252+
Permissions []Permission `json:"permissions"`
253+
}
254+
255+
// Permission is a single permission request.
256+
type Permission struct {
257+
Type string `json:"type"` // "fileWrite", "command"
258+
Description string `json:"description"`
259+
FilePath string `json:"filePath,omitempty"`
260+
Command string `json:"command,omitempty"`
261+
}
262+
263+
// RequestPermissionResult is the client's response.
264+
type RequestPermissionResult struct {
265+
Granted bool `json:"granted"`
266+
}
267+
268+
// --- File system types ---
269+
270+
// FSReadTextFileParams for fs/read_text_file request.
271+
type FSReadTextFileParams struct {
272+
Path string `json:"path"`
273+
}
274+
275+
// FSReadTextFileResult for fs/read_text_file response.
276+
type FSReadTextFileResult struct {
277+
Content string `json:"content"`
278+
}
279+
280+
// FSWriteTextFileParams for fs/write_text_file request.
281+
type FSWriteTextFileParams struct {
282+
Path string `json:"path"`
283+
Content string `json:"content"`
284+
}
285+
286+
// FSWriteTextFileResult for fs/write_text_file response.
287+
type FSWriteTextFileResult struct {
288+
Success bool `json:"success"`
289+
}
290+
291+
// --- Terminal types ---
292+
293+
// TerminalCreateParams for terminal/create request.
294+
type TerminalCreateParams struct {
295+
Command string `json:"command"`
296+
Cwd string `json:"cwd,omitempty"`
297+
}
298+
299+
// TerminalCreateResult for terminal/create response.
300+
type TerminalCreateResult struct {
301+
TerminalID string `json:"terminalId"`
302+
}
303+
304+
// TerminalWaitExitResult for terminal/wait_for_exit response.
305+
type TerminalWaitExitResult struct {
306+
ExitCode int `json:"exitCode"`
307+
}
308+
309+
// --- Helpers ---
310+
311+
// NewResponse creates a successful JSON-RPC response.
312+
func NewResponse(id interface{}, result interface{}) *Response {
313+
return &Response{
314+
JSONRPC: "2.0",
315+
ID: id,
316+
Result: result,
317+
}
318+
}
319+
320+
// NewErrorResponse creates an error JSON-RPC response.
321+
func NewErrorResponse(id interface{}, code int, message string) *Response {
322+
return &Response{
323+
JSONRPC: "2.0",
324+
ID: id,
325+
Error: &RPCError{
326+
Code: code,
327+
Message: message,
328+
},
329+
}
330+
}
331+
332+
// NewNotification creates a JSON-RPC notification.
333+
func NewNotification(method string, params interface{}) *Notification {
334+
return &Notification{
335+
JSONRPC: "2.0",
336+
Method: method,
337+
Params: params,
338+
}
339+
}

0 commit comments

Comments
 (0)