quorate/services/codex-bridge/rpc.go

123 lines
2.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os/exec"
"syscall"
)
type object = map[string]any
type rpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type event struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
Result json.RawMessage `json:"result"`
Error *rpcError `json:"error"`
}
type client struct {
cmd *exec.Cmd
input *json.Encoder
output *json.Decoder
id int
queued []event
}
func (b *bridge) start(ctx context.Context) (*client, error) {
args := []string{"app-server"}
if b.mcpURL != "" {
url, err := json.Marshal(b.mcpURL)
if err != nil {
return nil, err
}
args = append(args, "-c", "mcp_servers.gateway.url="+string(url), "-c", "mcp_servers.gateway.required=true")
}
cmd := exec.CommandContext(ctx, b.binary, args...)
cmd.Dir = b.dir
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }
in, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
out, err := cmd.StdoutPipe()
if err != nil {
_ = in.Close()
return nil, err
}
if err := cmd.Start(); err != nil {
_ = in.Close()
_ = out.Close()
return nil, err
}
c := &client{cmd: cmd, input: json.NewEncoder(in), output: json.NewDecoder(out)}
if _, err := c.call("initialize", object{"clientInfo": object{"name": "codex_bridge", "version": "0.1.0"}, "capabilities": object{"experimentalApi": true}}); err != nil {
c.close()
return nil, err
}
if err := c.input.Encode(object{"method": "initialized", "params": object{}}); err != nil {
c.close()
return nil, err
}
return c, nil
}
func (c *client) close() {
_ = syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL)
_ = c.cmd.Wait()
}
func (c *client) read() (event, error) {
var e event
if err := c.output.Decode(&e); err != nil {
if errors.Is(err, io.EOF) {
return e, errors.New("codex app-server closed its output")
}
return e, err
}
if len(e.ID) > 0 && e.Method != "" {
return e, fmt.Errorf("unsupported Codex client request: %s", e.Method)
}
return e, nil
}
func (c *client) call(method string, params object) (json.RawMessage, error) {
c.id++
id := fmt.Sprint(c.id)
if err := c.input.Encode(object{"id": c.id, "method": method, "params": params}); err != nil {
return nil, err
}
for {
e, err := c.read()
if err != nil {
return nil, err
}
if string(e.ID) != id {
c.queued = append(c.queued, e)
continue
}
if e.Error != nil {
return nil, fmt.Errorf("%s: %s", method, e.Error.Message)
}
return e.Result, nil
}
}
func (c *client) next() (event, error) {
if len(c.queued) == 0 {
return c.read()
}
e := c.queued[0]
c.queued = c.queued[1:]
return e, nil
}