-
Notifications
You must be signed in to change notification settings - Fork 129
Expand file tree
/
Copy pathserver.go
More file actions
209 lines (190 loc) · 6.4 KB
/
Copy pathserver.go
File metadata and controls
209 lines (190 loc) · 6.4 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
package server
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/xerrors"
"github.com/coder/agentapi/lib/httpapi"
"github.com/coder/agentapi/lib/logctx"
"github.com/coder/agentapi/lib/msgfmt"
"github.com/coder/agentapi/lib/termexec"
)
type AgentType = msgfmt.AgentType
const (
AgentTypeClaude AgentType = msgfmt.AgentTypeClaude
AgentTypeGoose AgentType = msgfmt.AgentTypeGoose
AgentTypeAider AgentType = msgfmt.AgentTypeAider
AgentTypeCodex AgentType = msgfmt.AgentTypeCodex
AgentTypeGemini AgentType = msgfmt.AgentTypeGemini
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
)
// exhaustiveness of this map is checked by the exhaustive linter
var agentTypeMap = map[AgentType]bool{
AgentTypeClaude: true,
AgentTypeGoose: true,
AgentTypeAider: true,
AgentTypeCodex: true,
AgentTypeGemini: true,
AgentTypeCustom: true,
}
func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) {
// if the agent type is provided, use it
castedAgentType := AgentType(agentTypeVar)
if _, ok := agentTypeMap[castedAgentType]; ok {
return castedAgentType, nil
}
if agentTypeVar != "" {
return AgentTypeCustom, fmt.Errorf("invalid agent type: %s", agentTypeVar)
}
// if the agent type is not provided, guess it from the first argument
castedFirstArg := AgentType(firstArg)
if _, ok := agentTypeMap[castedFirstArg]; ok {
return castedFirstArg, nil
}
return AgentTypeCustom, nil
}
func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) error {
agent := argsToPass[0]
agentTypeValue := viper.GetString(FlagType)
agentType, err := parseAgentType(agent, agentTypeValue)
if err != nil {
return xerrors.Errorf("failed to parse agent type: %w", err)
}
termWidth := viper.GetUint16(FlagTermWidth)
termHeight := viper.GetUint16(FlagTermHeight)
if termWidth < 10 {
return xerrors.Errorf("term width must be at least 10")
}
if termHeight < 10 {
return xerrors.Errorf("term height must be at least 10")
} else if agentType == AgentTypeCodex && termHeight > 930 {
logger.Warn(fmt.Sprintf("Term height is set to %d which may cause issues with Codex. Setting it to 930 instead.", termHeight))
termHeight = 930 // codex has a bug where the TUI distorts the screen if the height is too large, see: https://github.com/openai/codex/issues/1608
}
printOpenAPI := viper.GetBool(FlagPrintOpenAPI)
var process *termexec.Process
if printOpenAPI {
process = nil
} else {
process, err = httpapi.SetupProcess(ctx, httpapi.SetupProcessConfig{
Program: agent,
ProgramArgs: argsToPass[1:],
TerminalWidth: termWidth,
TerminalHeight: termHeight,
})
if err != nil {
return xerrors.Errorf("failed to setup process: %w", err)
}
}
port := viper.GetInt(FlagPort)
srv := httpapi.NewServer(ctx, httpapi.ServerConfig{
AgentType: agentType,
Process: process,
Port: port,
ChatBasePath: viper.GetString(FlagChatBasePath),
})
if printOpenAPI {
fmt.Println(srv.GetOpenAPI())
return nil
}
srv.StartSnapshotLoop(ctx)
logger.Info("Starting server on port", "port", port)
processExitCh := make(chan error, 1)
go func() {
defer close(processExitCh)
if err := process.Wait(); err != nil {
if errors.Is(err, termexec.ErrNonZeroExitCode) {
processExitCh <- xerrors.Errorf("========\n%s\n========\n: %w", strings.TrimSpace(process.ReadScreen()), err)
} else {
processExitCh <- xerrors.Errorf("failed to wait for process: %w", err)
}
}
if err := srv.Stop(ctx); err != nil {
logger.Error("Failed to stop server", "error", err)
}
}()
if err := srv.Start(); err != nil && err != context.Canceled && err != http.ErrServerClosed {
return xerrors.Errorf("failed to start server: %w", err)
}
select {
case err := <-processExitCh:
return xerrors.Errorf("agent exited with error: %w", err)
default:
}
return nil
}
var agentNames = (func() []string {
names := make([]string, 0, len(agentTypeMap))
for agentType := range agentTypeMap {
names = append(names, string(agentType))
}
sort.Strings(names)
return names
})()
type flagSpec struct {
name string
shorthand string
defaultValue any
usage string
flagType string
}
const (
FlagType = "type"
FlagPort = "port"
FlagPrintOpenAPI = "print-openapi"
FlagChatBasePath = "chat-base-path"
FlagTermWidth = "term-width"
FlagTermHeight = "term-height"
)
func CreateServerCmd() *cobra.Command {
serverCmd := &cobra.Command{
Use: "server [agent]",
Short: "Run the server",
Long: fmt.Sprintf("Run the server with the specified agent (one of: %s)", strings.Join(agentNames, ", ")),
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
ctx := logctx.WithLogger(context.Background(), logger)
if err := runServer(ctx, logger, cmd.Flags().Args()); err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
os.Exit(1)
}
},
}
flagSpecs := []flagSpec{
{FlagType, "t", "", fmt.Sprintf("Override the agent type (one of: %s, custom)", strings.Join(agentNames, ", ")), "string"},
{FlagPort, "p", 3284, "Port to run the server on", "int"},
{FlagPrintOpenAPI, "P", false, "Print the OpenAPI schema to stdout and exit", "bool"},
{FlagChatBasePath, "c", "/chat", "Base path for assets and routes used in the static files of the chat interface", "string"},
{FlagTermWidth, "W", uint16(80), "Width of the emulated terminal", "uint16"},
{FlagTermHeight, "H", uint16(1000), "Height of the emulated terminal", "uint16"},
}
for _, spec := range flagSpecs {
switch spec.flagType {
case "string":
serverCmd.Flags().StringP(spec.name, spec.shorthand, spec.defaultValue.(string), spec.usage)
case "int":
serverCmd.Flags().IntP(spec.name, spec.shorthand, spec.defaultValue.(int), spec.usage)
case "bool":
serverCmd.Flags().BoolP(spec.name, spec.shorthand, spec.defaultValue.(bool), spec.usage)
case "uint16":
serverCmd.Flags().Uint16P(spec.name, spec.shorthand, spec.defaultValue.(uint16), spec.usage)
default:
panic(fmt.Sprintf("unknown flag type: %s", spec.flagType))
}
if err := viper.BindPFlag(spec.name, serverCmd.Flags().Lookup(spec.name)); err != nil {
panic(fmt.Sprintf("failed to bind flag %s: %v", spec.name, err))
}
}
viper.SetEnvPrefix("AGENTAPI")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
return serverCmd
}