Run tasks on Codex beside Claude; drop turn caps and mid-turn snapshots
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/misc/byre/internal/codex"
|
||||
)
|
||||
|
||||
var codexToolItems = map[string]bool{
|
||||
codex.ThreadItemTypeCommandExecution: true,
|
||||
codex.ThreadItemTypeFileChange: true,
|
||||
codex.ThreadItemTypeDynamicToolCall: true,
|
||||
codex.ThreadItemTypeMcpToolCall: true,
|
||||
codex.ThreadItemTypeWebSearch: true,
|
||||
}
|
||||
|
||||
func (m *Manager) handleCodex(ctx context.Context, l *live, s *codex.Session, ev codex.Event) error {
|
||||
switch ev.Method {
|
||||
case codex.MethodItemAgentMessageDelta:
|
||||
n := codex.AgentMessageDeltaNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.store.Broadcast(m.event(l, agentActor, "agent.delta", map[string]any{"text": n.Delta}))
|
||||
return nil
|
||||
|
||||
case codex.MethodItemStarted:
|
||||
n := codex.ItemStartedNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !codexToolItems[n.Item.Type] {
|
||||
return nil
|
||||
}
|
||||
name, input, err := codexToolCall(n.Item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.emit(ctx, l, agentActor, "agent.tool_call", map[string]any{"id": n.Item.ID, "name": name, "input": input})
|
||||
|
||||
case codex.MethodItemCompleted:
|
||||
n := codex.ItemCompletedNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.codexItemCompleted(ctx, l, s, n.Item)
|
||||
|
||||
case codex.MethodThreadTokenUsageUpdated:
|
||||
n := codex.ThreadTokenUsageUpdatedNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.contextModel = l.model
|
||||
return m.updateContext(ctx, l, int(n.TokenUsage.Last.TotalTokens), int(n.TokenUsage.ModelContextWindow))
|
||||
|
||||
case codex.MethodAccountRateLimitsUpdated:
|
||||
n := codex.AccountRateLimitsUpdatedNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.rateLimits[l.vendor] = codexRateLimit(n.RateLimits)
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
|
||||
case codex.MethodTurnCompleted:
|
||||
n := codex.TurnCompletedNotification{}
|
||||
err := json.Unmarshal(ev.Params, &n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.codexTurnCompleted(ctx, l, s, n.Turn)
|
||||
|
||||
case codex.MethodError, codex.MethodWarning:
|
||||
params := map[string]any{}
|
||||
err := json.Unmarshal(ev.Params, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.emit(ctx, l, agentActor, "agent.vendor", map[string]any{"method": ev.Method, "params": params})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexToolCall(item codex.ThreadItem) (string, any, error) {
|
||||
switch item.Type {
|
||||
case codex.ThreadItemTypeCommandExecution:
|
||||
return "Bash", map[string]any{"command": item.Command}, nil
|
||||
case codex.ThreadItemTypeFileChange:
|
||||
return "apply_patch", map[string]any{"changes": item.Changes}, nil
|
||||
case codex.ThreadItemTypeWebSearch:
|
||||
return "WebSearch", map[string]any{"query": item.Query}, nil
|
||||
case codex.ThreadItemTypeMcpToolCall:
|
||||
tool := ""
|
||||
err := json.Unmarshal(item.Tool, &tool)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
args, err := decode(item.Arguments)
|
||||
return "mcp__" + item.Server + "__" + tool, args, err
|
||||
}
|
||||
tool := ""
|
||||
err := json.Unmarshal(item.Tool, &tool)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
args, err := decode(item.Arguments)
|
||||
return tool, args, err
|
||||
}
|
||||
|
||||
func codexStatus(item codex.ThreadItem) string {
|
||||
status := ""
|
||||
json.Unmarshal(item.Status, &status)
|
||||
return status
|
||||
}
|
||||
|
||||
func (m *Manager) codexItemCompleted(ctx context.Context, l *live, s *codex.Session, item codex.ThreadItem) error {
|
||||
switch item.Type {
|
||||
case codex.ThreadItemTypeUserMessage:
|
||||
return nil
|
||||
|
||||
case codex.ThreadItemTypeAgentMessage:
|
||||
m.mu.Lock()
|
||||
l.lastText = item.Text
|
||||
m.mu.Unlock()
|
||||
return m.emit(ctx, l, agentActor, "agent.message", map[string]any{"text": item.Text})
|
||||
|
||||
case codex.ThreadItemTypeReasoning:
|
||||
text := strings.TrimSpace(strings.Join(item.Summary, "\n\n"))
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
return m.emit(ctx, l, agentActor, "agent.thinking", map[string]any{"text": text})
|
||||
}
|
||||
|
||||
if !codexToolItems[item.Type] {
|
||||
raw, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]any{}
|
||||
err = json.Unmarshal(raw, &payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.emit(ctx, l, agentActor, "agent.vendor", payload)
|
||||
}
|
||||
|
||||
content, isError := codexToolResult(item)
|
||||
err := m.emit(ctx, l, agentActor, "agent.tool_result", map[string]any{"tool_use_id": item.ID, "is_error": isError, "content": content})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if l.owner != "" {
|
||||
return nil
|
||||
}
|
||||
text := m.deliverMidTurn(ctx, l)
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
err = s.Steer(ctx, text)
|
||||
if err != nil {
|
||||
slog.Error("steer mid-turn delivery", "session", l.id, "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func codexToolResult(item codex.ThreadItem) (string, bool) {
|
||||
status := codexStatus(item)
|
||||
switch item.Type {
|
||||
case codex.ThreadItemTypeCommandExecution:
|
||||
return item.AggregatedOutput, status != "completed" || item.ExitCode != 0
|
||||
case codex.ThreadItemTypeDynamicToolCall:
|
||||
texts := []string{}
|
||||
for _, c := range item.ContentItems {
|
||||
texts = append(texts, c.Text)
|
||||
}
|
||||
return strings.Join(texts, "\n"), !item.Success
|
||||
case codex.ThreadItemTypeMcpToolCall:
|
||||
if item.Error != nil {
|
||||
return item.Error.Message, true
|
||||
}
|
||||
return string(item.Result), false
|
||||
}
|
||||
return status, status != "completed"
|
||||
}
|
||||
|
||||
func codexRateLimit(r codex.RateLimitSnapshot) map[string]any {
|
||||
status := "allowed"
|
||||
if r.RateLimitReachedType != "" {
|
||||
status = "rejected"
|
||||
}
|
||||
windows := map[string]any{}
|
||||
for name, w := range map[string]*codex.RateLimitWindow{"primary": r.Primary, "secondary": r.Secondary} {
|
||||
if w == nil {
|
||||
continue
|
||||
}
|
||||
window := map[string]any{"utilization": float64(w.UsedPercent) / 100}
|
||||
if w.ResetsAt != 0 {
|
||||
window["resetsAt"] = w.ResetsAt
|
||||
}
|
||||
windows[name] = window
|
||||
}
|
||||
info := map[string]any{"status": status, "unifiedWindows": windows, "planType": r.PlanType}
|
||||
if r.RateLimitReachedType != "" {
|
||||
info["rateLimitType"] = r.RateLimitReachedType
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (m *Manager) codexTurnCompleted(ctx context.Context, l *live, s *codex.Session, turn codex.Turn) error {
|
||||
m.mu.Lock()
|
||||
final := l.lastText
|
||||
m.mu.Unlock()
|
||||
|
||||
if turn.Status == codex.TurnStatusCompleted && l.owner == "" && m.takeBounce(l, final) {
|
||||
err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
l.lastText = ""
|
||||
m.mu.Unlock()
|
||||
_, err = s.Send(ctx, proseQuestionNudge)
|
||||
return err
|
||||
}
|
||||
|
||||
res := endResult{
|
||||
subtype: string(turn.Status),
|
||||
isError: turn.Status == codex.TurnStatusFailed,
|
||||
result: final,
|
||||
}
|
||||
if turn.Error != nil {
|
||||
res.result = turn.Error.Message
|
||||
info := ""
|
||||
json.Unmarshal(turn.Error.CodexErrorInfo, &info)
|
||||
if info == "usageLimitExceeded" || info == "rateLimitExceeded" {
|
||||
m.mu.Lock()
|
||||
l.rateLimited = map[string]any{"status": "rejected", "rateLimitType": info}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
res.payload = map[string]any{
|
||||
"subtype": res.subtype,
|
||||
"is_error": res.isError,
|
||||
"result": res.result,
|
||||
"vendor_uuid": turn.ID,
|
||||
}
|
||||
return m.turnEnded(ctx, l, res)
|
||||
}
|
||||
Reference in New Issue
Block a user