Run tasks on Codex beside Claude; drop turn caps and mid-turn snapshots
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user