2026-10-15 14:55:58 -07:00
|
|
|
package agents
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"log/slog"
|
2026-10-15 22:30:32 -07:00
|
|
|
"os"
|
2026-10-15 14:55:58 -07:00
|
|
|
"strings"
|
2026-10-15 22:30:32 -07:00
|
|
|
"time"
|
2026-10-15 14:55:58 -07:00
|
|
|
|
2026-10-15 22:30:32 -07:00
|
|
|
"g.fc.run/misc/byre/internal/api"
|
2026-10-15 14:55:58 -07:00
|
|
|
"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
|
|
|
|
|
}
|
2026-10-15 22:30:32 -07:00
|
|
|
m.setCodexRateLimit(n.RateLimits)
|
2026-10-15 14:55:58 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 18:33:18 -07:00
|
|
|
if l.reviewID != "" {
|
2026-10-15 14:55:58 -07:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-16 14:18:40 -07:00
|
|
|
mt := m.deliverMidTurn(ctx, l)
|
|
|
|
|
if mt.text == "" {
|
2026-10-15 14:55:58 -07:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-16 14:18:40 -07:00
|
|
|
err = s.Steer(ctx, mt.text)
|
2026-10-15 14:55:58 -07:00
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("steer mid-turn delivery", "session", l.id, "err", err)
|
2026-10-16 14:18:40 -07:00
|
|
|
m.restoreMidTurn(ctx, l, mt)
|
2026-10-15 14:55:58 -07:00
|
|
|
}
|
|
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 22:30:32 -07:00
|
|
|
func (m *Manager) readCodexRateLimit() {
|
|
|
|
|
err := os.MkdirAll(m.codexHome, 0o700)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("read codex rate limits", "err", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
r, err := codex.ReadRateLimits(ctx, m.codexHome, []string{"CODEX_HOME=" + m.codexHome})
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("read codex rate limits", "err", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
m.setCodexRateLimit(r)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) setCodexRateLimit(r codex.RateLimitSnapshot) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.rateLimits[api.Codex] = codexRateLimit(r)
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
m.store.Broadcast(api.Event{Kind: "admin.health", Actor: "byre", Payload: map[string]any{}})
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 14:55:58 -07:00
|
|
|
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
|
|
|
|
|
}
|
2026-10-16 10:47:53 -07:00
|
|
|
if w.WindowDurationMins != 0 {
|
|
|
|
|
window["windowMins"] = w.WindowDurationMins
|
|
|
|
|
}
|
2026-10-15 14:55:58 -07:00
|
|
|
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()
|
|
|
|
|
|
2026-10-15 16:33:17 -07:00
|
|
|
if turn.Status == codex.TurnStatusCompleted && l.implementer() && m.takeBounce(l, final) {
|
2026-10-15 14:55:58 -07:00
|
|
|
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)
|
|
|
|
|
}
|