Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
717 lines
20 KiB
Go
717 lines
20 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func readSessionTestdataJSON(t *testing.T, name string) map[string]any {
|
|
t.Helper()
|
|
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller failed")
|
|
}
|
|
path := filepath.Clean(filepath.Join(filepath.Dir(file), "testdata", name))
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read session testdata %s: %v", path, err)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(raw, &body); err != nil {
|
|
t.Fatalf("unmarshal session testdata %s: %v", path, err)
|
|
}
|
|
return body
|
|
}
|
|
|
|
func testSession() Session {
|
|
return Session{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderNexight,
|
|
ProviderSessionID: "agent-session-1",
|
|
CWD: "/workspace/room-1",
|
|
Status: SessionStatusReady,
|
|
}
|
|
}
|
|
|
|
type scriptedACPTransport struct {
|
|
mu sync.Mutex
|
|
specs []ProcessSpec
|
|
conn *scriptedACPConnection
|
|
}
|
|
|
|
func newScriptedACPTransport() *scriptedACPTransport {
|
|
return &scriptedACPTransport{conn: &scriptedACPConnection{
|
|
recv: make(chan ProcessFrame, 32),
|
|
supportsSessionRestore: true,
|
|
supportsHTTPMCP: true,
|
|
respondSetMode: true,
|
|
}}
|
|
}
|
|
|
|
func (t *scriptedACPTransport) Start(_ context.Context, spec ProcessSpec) (ProcessConnection, error) {
|
|
t.mu.Lock()
|
|
t.specs = append(t.specs, spec)
|
|
t.mu.Unlock()
|
|
return t.conn, nil
|
|
}
|
|
|
|
type scriptedACPConnection struct {
|
|
mu sync.Mutex
|
|
sent [][]byte
|
|
setConfigOptionSnapshots []map[string]any
|
|
configOptions []map[string]any
|
|
recv chan ProcessFrame
|
|
supportsSessionRestore bool
|
|
supportsHTTPMCP bool
|
|
respondSetMode bool
|
|
authRequiredOnNewSession bool
|
|
commandUpdateOnNewSession bool
|
|
commandUpdateOnLoadSession bool
|
|
loadSessionError *acpError
|
|
promptPermission bool
|
|
promptKind string
|
|
pauseBeforePromptResult chan struct{}
|
|
promptFinalContent string
|
|
pendingPermissionPromptID json.RawMessage
|
|
selectedPermissionOption string
|
|
selectedInteractiveResult map[string]any
|
|
appServerTurnStatus string
|
|
}
|
|
|
|
func (c *scriptedACPConnection) Send(data []byte) error {
|
|
c.mu.Lock()
|
|
c.sent = append(c.sent, append([]byte(nil), data...))
|
|
c.mu.Unlock()
|
|
|
|
for _, line := range acpScanLines(data) {
|
|
var message struct {
|
|
ID json.RawMessage `json:"id"`
|
|
Method string `json:"method"`
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
_ = json.Unmarshal([]byte(line), &message)
|
|
if c.handleAppServerMessage(line, message.ID, message.Method) {
|
|
continue
|
|
}
|
|
switch message.Method {
|
|
case acpMethodInitialize:
|
|
result := map[string]any{
|
|
"protocolVersion": acpProtocolVersion,
|
|
"agentInfo": map[string]any{
|
|
"name": "codex-acp",
|
|
"title": "Codex",
|
|
},
|
|
}
|
|
if c.supportsSessionRestore || c.supportsHTTPMCP {
|
|
capabilities := map[string]any{}
|
|
if c.supportsSessionRestore {
|
|
capabilities["loadSession"] = true
|
|
}
|
|
if c.supportsHTTPMCP {
|
|
capabilities["mcpCapabilities"] = map[string]any{"http": true}
|
|
}
|
|
result["agentCapabilities"] = capabilities
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": result,
|
|
})
|
|
case acpMethodNewSession:
|
|
if c.authRequiredOnNewSession {
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"error": map[string]any{
|
|
"code": -32001,
|
|
"message": "auth required",
|
|
"data": map[string]any{
|
|
"authRequired": true,
|
|
},
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
if c.commandUpdateOnNewSession {
|
|
c.sendAvailableCommandsUpdate()
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"configOptions": c.defaultConfigOptions(),
|
|
},
|
|
})
|
|
case acpMethodLoadSession, acpMethodResume:
|
|
if c.commandUpdateOnLoadSession {
|
|
c.sendAvailableCommandsUpdate()
|
|
}
|
|
if c.loadSessionError != nil {
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"error": c.loadSessionError,
|
|
})
|
|
return nil
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": map[string]any{
|
|
"configOptions": c.defaultConfigOptions(),
|
|
},
|
|
})
|
|
case acpMethodSetMode:
|
|
if c.respondSetMode {
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": map[string]any{},
|
|
})
|
|
}
|
|
case acpMethodSetConfigOption:
|
|
var request struct {
|
|
Params map[string]any `json:"params"`
|
|
}
|
|
_ = json.Unmarshal([]byte(line), &request)
|
|
c.mu.Lock()
|
|
c.setConfigOptionSnapshots = append(c.setConfigOptionSnapshots, clonePayload(request.Params))
|
|
c.mu.Unlock()
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": map[string]any{},
|
|
})
|
|
case acpMethodPrompt:
|
|
if c.promptPermission || c.promptKind != "" {
|
|
c.mu.Lock()
|
|
c.pendingPermissionPromptID = append(json.RawMessage(nil), message.ID...)
|
|
c.mu.Unlock()
|
|
toolCall, options := c.promptRequest()
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "permission-1",
|
|
"method": acpMethodPermission,
|
|
"params": map[string]any{
|
|
"toolCall": toolCall,
|
|
"options": options,
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
if c.pauseBeforePromptResult != nil {
|
|
<-c.pauseBeforePromptResult
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "tool_call",
|
|
"toolCallId": "tool-1",
|
|
"title": "Reading files",
|
|
"kind": "read",
|
|
"status": "in_progress",
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "session_info_update",
|
|
"title": "Inspect repository structure",
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_thought_chunk",
|
|
"content": map[string]any{
|
|
"type": "text",
|
|
"text": "Need ",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_thought_chunk",
|
|
"content": map[string]any{
|
|
"type": "text",
|
|
"text": "context.",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_message_chunk",
|
|
"content": map[string]any{
|
|
"type": "text",
|
|
"text": "I'll ",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_message_chunk",
|
|
"content": map[string]any{
|
|
"type": "text",
|
|
"text": "check ",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_message_chunk",
|
|
"content": map[string]any{
|
|
"type": "text",
|
|
"text": "the repo.",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
result := map[string]any{
|
|
"stopReason": "end_turn",
|
|
}
|
|
if strings.TrimSpace(c.promptFinalContent) != "" {
|
|
result["content"] = []map[string]any{{
|
|
"type": "text",
|
|
"text": c.promptFinalContent,
|
|
}}
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"result": result,
|
|
})
|
|
default:
|
|
if (c.promptPermission || c.promptKind != "") && acpRequestID(message.ID) == "permission-1" {
|
|
var response struct {
|
|
Result struct {
|
|
Outcome struct {
|
|
OptionID string `json:"optionId"`
|
|
Outcome string `json:"outcome"`
|
|
Payload map[string]any `json:"payload"`
|
|
} `json:"outcome"`
|
|
} `json:"result"`
|
|
}
|
|
_ = json.Unmarshal([]byte(line), &response)
|
|
c.mu.Lock()
|
|
c.selectedPermissionOption = response.Result.Outcome.OptionID
|
|
c.selectedInteractiveResult = map[string]any{
|
|
"outcome": response.Result.Outcome.Outcome,
|
|
"optionId": response.Result.Outcome.OptionID,
|
|
"payload": response.Result.Outcome.Payload,
|
|
}
|
|
promptID := append(json.RawMessage(nil), c.pendingPermissionPromptID...)
|
|
c.mu.Unlock()
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": promptID,
|
|
"result": map[string]any{
|
|
"stopReason": "end_turn",
|
|
},
|
|
})
|
|
continue
|
|
}
|
|
if strings.TrimSpace(message.Method) != "" && len(message.ID) > 0 {
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": message.ID,
|
|
"error": map[string]any{
|
|
"code": -32601,
|
|
"message": "method not found: " + message.Method,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestScriptedACPConnectionRejectsUnknownRequests(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
conn := newScriptedACPTransport().conn
|
|
if err := conn.Send([]byte("{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"future/capability\"}\n")); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
frame, err := conn.Recv()
|
|
if err != nil {
|
|
t.Fatalf("Recv: %v", err)
|
|
}
|
|
var response struct {
|
|
ID int `json:"id"`
|
|
Error *acpError `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(frame.Stdout, &response); err != nil {
|
|
t.Fatalf("Unmarshal: %v", err)
|
|
}
|
|
if response.ID != 99 || response.Error == nil || response.Error.Code != -32601 {
|
|
t.Fatalf("response = %#v, want method-not-found for id 99", response)
|
|
}
|
|
}
|
|
|
|
// handleAppServerMessage lets the shared scripted connection answer the codex
|
|
// app-server protocol next to ACP, so controller tests can exercise the
|
|
// app-server-backed codex adapter with the same fake. Returns true when the
|
|
// message was consumed.
|
|
func (c *scriptedACPConnection) handleAppServerMessage(line string, id json.RawMessage, method string) bool {
|
|
switch method {
|
|
case appServerMethodInitialized:
|
|
return true
|
|
case appServerMethodAccountRead:
|
|
requiresAuth := c.authRequiredOnNewSession
|
|
result := map[string]any{
|
|
"requiresOpenaiAuth": requiresAuth,
|
|
}
|
|
if !requiresAuth {
|
|
result["account"] = map[string]any{"type": "chatgpt", "planType": "pro"}
|
|
}
|
|
c.sendJSON(map[string]any{"id": id, "result": result})
|
|
return true
|
|
case appServerMethodModelList:
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{"data": []any{}}})
|
|
return true
|
|
case appServerMethodCollaborationModeList:
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{"data": []any{}}})
|
|
return true
|
|
case appServerMethodRateLimitsRead:
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{"rateLimits": map[string]any{}}})
|
|
return true
|
|
case appServerMethodThreadStart:
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{
|
|
"thread": map[string]any{"id": "codex-thread-1"},
|
|
},
|
|
})
|
|
return true
|
|
case appServerMethodThreadResume:
|
|
var request struct {
|
|
Params map[string]any `json:"params"`
|
|
}
|
|
_ = json.Unmarshal([]byte(line), &request)
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{
|
|
"thread": map[string]any{"id": asString(request.Params["threadId"])},
|
|
},
|
|
})
|
|
return true
|
|
case appServerMethodTurnStart:
|
|
c.mu.Lock()
|
|
c.appServerTurnStatus = "completed"
|
|
c.mu.Unlock()
|
|
// Mirror the real app-server: respond immediately with the
|
|
// inProgress turn; output streams as notifications afterwards.
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{
|
|
"turn": map[string]any{"id": "turn-1", "status": "inProgress", "items": []any{}},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyTurnStarted,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1",
|
|
"turn": map[string]any{"id": "turn-1", "status": "inProgress", "items": []any{}},
|
|
},
|
|
})
|
|
if c.promptPermission || c.promptKind != "" {
|
|
c.sendJSON(map[string]any{
|
|
"id": "permission-1",
|
|
"method": appServerMethodCommandApproval,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1",
|
|
"turnId": "turn-1",
|
|
"itemId": "item-cmd",
|
|
"command": "make test",
|
|
"cwd": "/workspace",
|
|
"startedAtMs": 1750000000000,
|
|
},
|
|
})
|
|
return true
|
|
}
|
|
if c.pauseBeforePromptResult != nil {
|
|
<-c.pauseBeforePromptResult
|
|
}
|
|
for _, delta := range []string{"Need ", "context."} {
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyReasoningDelta,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1", "turnId": "turn-1",
|
|
"itemId": "item-think", "contentIndex": 0, "delta": delta,
|
|
},
|
|
})
|
|
}
|
|
for _, delta := range []string{"I'll ", "check ", "the repo."} {
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyAgentMessageDelta,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1", "turnId": "turn-1",
|
|
"itemId": "item-msg", "delta": delta,
|
|
},
|
|
})
|
|
}
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyItemStarted,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1", "turnId": "turn-1", "startedAtMs": 1750000000000,
|
|
"item": map[string]any{
|
|
"type": "commandExecution", "id": "item-cmd",
|
|
"command": "ls -la", "cwd": "/workspace", "status": "inProgress",
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyThreadNameUpdated,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1",
|
|
"threadName": "Inspect repository structure",
|
|
},
|
|
})
|
|
c.completeAppServerTurn()
|
|
return true
|
|
case appServerMethodThreadGoalGet:
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{"goal": nil},
|
|
})
|
|
return true
|
|
case appServerMethodTurnInterrupt:
|
|
c.mu.Lock()
|
|
c.appServerTurnStatus = "interrupted"
|
|
c.mu.Unlock()
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{}})
|
|
c.completeAppServerTurn()
|
|
return true
|
|
case appServerMethodTurnSteer:
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{"turnId": "turn-1"}})
|
|
return true
|
|
case appServerMethodThreadCompact:
|
|
c.sendJSON(map[string]any{"id": id, "result": map[string]any{}})
|
|
return true
|
|
case appServerMethodThreadRollback:
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{"thread": map[string]any{"id": "codex-thread-1"}},
|
|
})
|
|
return true
|
|
case appServerMethodReviewStart:
|
|
c.sendJSON(map[string]any{
|
|
"id": id,
|
|
"result": map[string]any{
|
|
"reviewThreadId": "codex-thread-1",
|
|
"turn": map[string]any{
|
|
"id": "turn-review", "status": "completed",
|
|
"items": []any{map[string]any{"type": "agentMessage", "id": "item-review", "text": "Review finished."}},
|
|
},
|
|
},
|
|
})
|
|
return true
|
|
case "":
|
|
if acpRequestID(id) != "permission-1" {
|
|
return false
|
|
}
|
|
var response struct {
|
|
Result struct {
|
|
Decision string `json:"decision"`
|
|
} `json:"result"`
|
|
Error json.RawMessage `json:"error"`
|
|
}
|
|
_ = json.Unmarshal([]byte(line), &response)
|
|
if response.Result.Decision == "" {
|
|
if len(response.Error) > 0 {
|
|
// App-server approval rejected (for example on cancel); the
|
|
// turn finishes through turn/interrupt instead.
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
optionID := map[string]string{
|
|
"accept": "allow_once",
|
|
"acceptForSession": "allow_always",
|
|
"decline": "reject_once",
|
|
"cancel": "reject_always",
|
|
}[response.Result.Decision]
|
|
c.mu.Lock()
|
|
c.selectedPermissionOption = optionID
|
|
c.mu.Unlock()
|
|
c.completeAppServerTurn()
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// completeAppServerTurn finishes the in-flight app-server turn the way the
|
|
// real server does: with a turn/completed notification (the turn/start RPC
|
|
// already responded immediately).
|
|
func (c *scriptedACPConnection) completeAppServerTurn() {
|
|
c.mu.Lock()
|
|
status := firstNonEmpty(c.appServerTurnStatus, "completed")
|
|
finalContent := firstNonEmpty(strings.TrimSpace(c.promptFinalContent), "I'll check the repo.")
|
|
c.mu.Unlock()
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyItemCompleted,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1", "turnId": "turn-1", "completedAtMs": 1750000001000,
|
|
"item": map[string]any{
|
|
"type": "commandExecution", "id": "item-cmd",
|
|
"command": "ls -la", "cwd": "/workspace", "status": "completed",
|
|
"aggregatedOutput": "README.md\n", "exitCode": 0,
|
|
},
|
|
},
|
|
})
|
|
c.sendJSON(map[string]any{
|
|
"method": appServerNotifyTurnCompleted,
|
|
"params": map[string]any{
|
|
"threadId": "codex-thread-1",
|
|
"turn": map[string]any{
|
|
"id": "turn-1",
|
|
"status": status,
|
|
"items": []any{
|
|
map[string]any{"type": "agentMessage", "id": "item-msg", "text": finalContent},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (c *scriptedACPConnection) Recv() (ProcessFrame, error) {
|
|
frame, ok := <-c.recv
|
|
if !ok {
|
|
return ProcessFrame{}, io.EOF
|
|
}
|
|
return frame, nil
|
|
}
|
|
|
|
func (c *scriptedACPConnection) Close() error {
|
|
close(c.recv)
|
|
return nil
|
|
}
|
|
|
|
func (c *scriptedACPConnection) sendAvailableCommandsUpdate() {
|
|
c.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": "codex-acp-session-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "available_commands_update",
|
|
"availableCommands": []any{
|
|
map[string]any{
|
|
"name": "web",
|
|
"description": "Search the web",
|
|
"input": map[string]any{
|
|
"hint": "query",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func (c *scriptedACPConnection) sendJSON(value any) {
|
|
raw, _ := json.Marshal(value)
|
|
raw = append(raw, '\n')
|
|
c.recv <- ProcessFrame{Stdout: raw}
|
|
}
|
|
|
|
func (c *scriptedACPConnection) permissionOptionID() string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.selectedPermissionOption
|
|
}
|
|
|
|
func (c *scriptedACPConnection) defaultConfigOptions() []map[string]any {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return cloneConfigOptionDescriptors(c.configOptions)
|
|
}
|
|
|
|
func (c *scriptedACPConnection) promptRequest() (map[string]any, []map[string]any) {
|
|
switch c.promptKind {
|
|
case "ask-user":
|
|
return map[string]any{
|
|
"toolCallId": "interactive-ask-1",
|
|
"title": "AskUserQuestion",
|
|
"input": map[string]any{
|
|
"questions": []map[string]any{{
|
|
"id": "render-path",
|
|
"header": "Renderer",
|
|
"question": "Which renderer should we use?",
|
|
"options": []map[string]any{
|
|
{"label": "Renderer A", "description": "Shared transcript renderer"},
|
|
{"label": "Renderer B", "description": "Legacy room renderer"},
|
|
},
|
|
}},
|
|
},
|
|
}, nil
|
|
case "exit-plan":
|
|
return map[string]any{
|
|
"toolCallId": "interactive-plan-1",
|
|
"title": "ExitPlanMode",
|
|
"input": map[string]any{
|
|
"plan": "Implement the shared renderer",
|
|
},
|
|
}, nil
|
|
default:
|
|
return map[string]any{
|
|
"toolCallId": "approval-1",
|
|
"title": "Run command",
|
|
}, []map[string]any{{
|
|
"optionId": "allow_once",
|
|
"label": "Allow once",
|
|
"kind": "allow_once",
|
|
}, {
|
|
"optionId": "reject",
|
|
"label": "No, continue without running",
|
|
"kind": "reject_once",
|
|
}, {
|
|
"optionId": "abort",
|
|
"label": "No, and tell Codex what to do differently",
|
|
"kind": "reject_once",
|
|
}}
|
|
}
|
|
}
|