26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
138 lines
4.0 KiB
Go
138 lines
4.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (a *opencodeAgent) ensureServer(ctx context.Context, cwd string) (string, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
if a.server != nil {
|
|
return a.server.baseURL(), nil
|
|
}
|
|
port, err := getAvailablePort()
|
|
if err != nil {
|
|
return "", fmt.Errorf("opencode port: %w", err)
|
|
}
|
|
args := buildOpencodeServeArgs(a.extraArgs, port)
|
|
srv, err := startServerWithPort(ctx, "opencode", a.bin, args, cwd, "/global/health", port)
|
|
if err != nil {
|
|
return "", fmt.Errorf("opencode server: %w", err)
|
|
}
|
|
a.server = srv
|
|
return srv.baseURL(), nil
|
|
}
|
|
|
|
// buildOpencodeServeArgs builds `opencode serve`'s argv with user-supplied
|
|
// extras inserted after the "serve" subcommand and before the managed flags.
|
|
func buildOpencodeServeArgs(extraArgs []string, port int) []string {
|
|
args := make([]string, 0, len(extraArgs)+6)
|
|
args = append(args, "serve")
|
|
args = append(args, extraArgs...)
|
|
args = append(args, "--hostname", "127.0.0.1", "--port", fmt.Sprintf("%d", port), "--print-logs")
|
|
return args
|
|
}
|
|
|
|
func (a *opencodeAgent) createSession(ctx context.Context, baseURL, cwd string) (string, error) {
|
|
body := map[string]any{
|
|
"directory": cwd,
|
|
"permission": []map[string]string{
|
|
{"permission": "*", "pattern": "*", "action": "allow"},
|
|
},
|
|
}
|
|
resp, err := doJSON(ctx, http.MethodPost, baseURL+"/session", nil, body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("opencode create session: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(resp, &result); err != nil {
|
|
return "", fmt.Errorf("opencode create session parse: %w", err)
|
|
}
|
|
return result.ID, nil
|
|
}
|
|
|
|
func (a *opencodeAgent) connectEventStream(ctx context.Context, baseURL string) (io.ReadCloser, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/global/event", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opencode event stream request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "text/event-stream")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opencode event stream: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("opencode event stream failed with %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return resp.Body, nil
|
|
}
|
|
|
|
func (a *opencodeAgent) sendMessage(ctx context.Context, baseURL, sessionID, prompt string, schema json.RawMessage) (*opencodeMessageResponse, error) {
|
|
body := map[string]any{
|
|
"role": "user",
|
|
"parts": []map[string]string{{"type": "text", "text": prompt}},
|
|
}
|
|
if len(schema) > 0 {
|
|
body["format"] = map[string]any{
|
|
"type": "json_schema",
|
|
"schema": json.RawMessage(schema),
|
|
"retryCount": 2,
|
|
}
|
|
}
|
|
|
|
respBytes, err := doJSON(ctx, http.MethodPost, baseURL+"/session/"+sessionID+"/message", nil, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var resp opencodeMessageResponse
|
|
if err := json.Unmarshal(respBytes, &resp); err != nil {
|
|
return nil, fmt.Errorf("opencode message parse: %w", err)
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
func (a *opencodeAgent) abortSession(baseURL, sessionID string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
doJSON(ctx, http.MethodPost, baseURL+"/session/"+sessionID+"/abort", nil, nil)
|
|
}
|
|
|
|
func (a *opencodeAgent) deleteSession(baseURL, sessionID string) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, baseURL+"/session/"+sessionID, nil)
|
|
if req != nil {
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err == nil && resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildOpencodePrompt appends schema instructions to the prompt.
|
|
func buildOpencodePrompt(prompt string, schema json.RawMessage) string {
|
|
return strings.Join([]string{
|
|
prompt,
|
|
"",
|
|
"When you finish, reply with only valid JSON.",
|
|
"Do not wrap the JSON in markdown fences.",
|
|
"Do not include any prose before or after the JSON.",
|
|
"The JSON must match this schema exactly: " + string(schema),
|
|
}, "\n")
|
|
}
|