Run tasks on Codex beside Claude; drop turn caps and mid-turn snapshots

This commit is contained in:
Ian Gulliver
2026-10-15 14:55:58 -07:00
parent 17898b0191
commit 74707f6398
53 changed files with 1904 additions and 1295 deletions
+98 -1
View File
@@ -17,16 +17,22 @@ import (
"sync"
"sync/atomic"
"time"
"g.fc.run/misc/byre/internal/mcp"
)
type Options struct {
Cwd string
Model string
Resume string
ForkFrom string
ForkAt string
GitName string
GitEmail string
Env []string
Guidance string
Reminder string
MCPServers []*mcp.Server
Transcript io.Writer
OnBadLine func(line []byte, reason string)
}
@@ -101,6 +107,9 @@ type Session struct {
model string
userAgent string
servers map[string]*mcp.Server
reminder string
onBadLine func(line []byte, reason string)
lastLine atomic.Int64
}
@@ -144,8 +153,13 @@ func newSession(opts Options, stdin io.WriteCloser, stdout io.Reader) *Session {
done: make(chan struct{}),
transcript: opts.Transcript,
pending: map[int64]chan rpcResult{},
servers: map[string]*mcp.Server{},
reminder: opts.Reminder,
onBadLine: opts.OnBadLine,
}
for _, srv := range opts.MCPServers {
s.servers[srv.Name] = srv
}
s.lastLine.Store(time.Now().UnixNano())
go s.readLoop(stdout)
return s
@@ -154,7 +168,8 @@ func newSession(opts Options, stdin io.WriteCloser, stdout io.Reader) *Session {
func (s *Session) open(ctx context.Context, opts Options) error {
init := InitializeResponse{}
err := s.Call(ctx, MethodInitialize, InitializeParams{
ClientInfo: ClientInfo{Name: "byre", Title: "byre", Version: "0.0.0"},
ClientInfo: ClientInfo{Name: "byre", Title: "byre", Version: "0.0.0"},
Capabilities: &InitializeCapabilities{ExperimentalApi: true},
}, &init)
if err != nil {
return fmt.Errorf("initialize: %w", err)
@@ -186,6 +201,24 @@ func (s *Session) open(ctx context.Context, opts Options) error {
return nil
}
if opts.ForkFrom != "" {
resp := ThreadForkResponse{}
err = s.Call(ctx, MethodThreadFork, ThreadForkParams{
ThreadID: opts.ForkFrom,
LastTurnID: opts.ForkAt,
Cwd: opts.Cwd,
Model: opts.Model,
ApprovalPolicy: json.RawMessage(`"never"`),
Sandbox: SandboxModeDangerFullAccess,
DeveloperInstructions: opts.Guidance,
}, &resp)
if err != nil {
return fmt.Errorf("fork thread: %w", err)
}
s.setThread(resp.Thread.ID)
return nil
}
resp := ThreadStartResponse{}
err = s.Call(ctx, MethodThreadStart, ThreadStartParams{
Cwd: opts.Cwd,
@@ -193,6 +226,7 @@ func (s *Session) open(ctx context.Context, opts Options) error {
ApprovalPolicy: json.RawMessage(`"never"`),
Sandbox: SandboxModeDangerFullAccess,
DeveloperInstructions: opts.Guidance,
DynamicTools: dynamicTools(opts.MCPServers),
}, &resp)
if err != nil {
return fmt.Errorf("start thread: %w", err)
@@ -201,6 +235,28 @@ func (s *Session) open(ctx context.Context, opts Options) error {
return nil
}
func dynamicTools(servers []*mcp.Server) []DynamicToolSpec {
specs := []DynamicToolSpec{}
for _, srv := range servers {
tools := []DynamicToolNamespaceTool{}
for _, t := range srv.Tools {
tools = append(tools, DynamicToolNamespaceTool{
Type: DynamicToolSpecTypeFunction,
Name: t.Name,
Description: t.Description,
InputSchema: t.InputSchema,
})
}
specs = append(specs, DynamicToolSpec{
Type: DynamicToolSpecTypeNamespace,
Name: srv.Name,
Description: srv.Name + "'s tools",
Tools: tools,
})
}
return specs
}
func (s *Session) setThread(id string) {
s.stateMu.Lock()
s.threadID = id
@@ -282,6 +338,11 @@ func (s *Session) Send(ctx context.Context, text string, images ...Image) (Turn,
params := TurnStartParams{ThreadID: s.threadID, Input: input, Model: s.model}
s.model = ""
s.stateMu.Unlock()
if s.reminder != "" {
params.AdditionalContext = map[string]AdditionalContextEntry{
"byre": {Kind: AdditionalContextKindApplication, Value: s.reminder},
}
}
resp := TurnStartResponse{}
err := s.Call(ctx, MethodTurnStart, params, &resp)
@@ -308,6 +369,17 @@ func (s *Session) Interrupt(ctx context.Context) error {
return s.Call(ctx, MethodTurnInterrupt, params, nil)
}
func (s *Session) Steer(ctx context.Context, text string) error {
s.stateMu.Lock()
params := TurnSteerParams{ThreadID: s.threadID, ExpectedTurnID: s.turnID, Input: []UserInput{{Type: UserInputTypeText, Text: text}}}
s.stateMu.Unlock()
if params.ExpectedTurnID == "" {
return errors.New("no turn in progress")
}
return s.Call(ctx, MethodTurnSteer, params, nil)
}
func (s *Session) Compact(ctx context.Context) error {
return s.Call(ctx, MethodThreadCompactStart, ThreadCompactStartParams{ThreadID: s.ID()}, nil)
}
@@ -540,10 +612,35 @@ func (s *Session) answerServerRequest(m message) (any, error) {
return CommandExecutionRequestApprovalResponse{Decision: json.RawMessage(`"accept"`)}, nil
case MethodItemFileChangeRequestApproval:
return FileChangeRequestApprovalResponse{Decision: "accept"}, nil
case MethodItemToolCall:
return s.callTool(m.Params)
}
return nil, fmt.Errorf("unsupported server request %s", m.Method)
}
func (s *Session) callTool(raw json.RawMessage) (any, error) {
p := DynamicToolCallParams{}
err := json.Unmarshal(raw, &p)
if err != nil {
return nil, err
}
srv := s.servers[p.Namespace]
if srv == nil {
return nil, fmt.Errorf("unknown tool namespace %q", p.Namespace)
}
t, ok := srv.Tool(p.Tool)
if !ok {
return nil, fmt.Errorf("unknown tool %s in %s", p.Tool, p.Namespace)
}
text, err := t.Handler(context.Background(), p.Arguments)
if err != nil {
return DynamicToolCallResponse{ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: err.Error()}}}, nil
}
return DynamicToolCallResponse{Success: true, ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: text}}}, nil
}
func (s *Session) badLine(line []byte, reason string) {
slog.Warn("bad codex line", "reason", reason, "line", string(line))
if s.onBadLine != nil {