iop/apps/node/internal/adapters/cli/codex_exec.go
toki 157a8a7076 feat: add console events, codex execution support and update config
- add console_events.go for edge console event handling
- add codex_exec.go for CLI codex execution
- add codex_exec_test.go for persistent CLI tests
- update console.go and oneshot.go with new features
- update config and edge.yaml for new settings
- update README files for edge and node apps
2026-05-04 07:55:08 +09:00

120 lines
3.4 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"iop/apps/node/internal/runtime"
"iop/packages/config"
)
var (
codexSessionIDPattern = regexp.MustCompile(`(?mi)^session id:\s*(\S+)\s*$`)
codexThreadStartedRegex = regexp.MustCompile(`\{[^{}]*"type"\s*:\s*"thread\.started"[^{}]*\}`)
)
func isCodexExecProfile(profile config.CLIProfileConf) bool {
return filepath.Base(profile.Command) == "codex" && len(profile.Args) > 0 && profile.Args[0] == "exec"
}
func (c *CLI) executeCodexExec(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error {
sess, err := c.resolveCodexExecSession(spec)
if err != nil {
return err
}
sess.mu.Lock()
defer sess.mu.Unlock()
prompt := extractPrompt(spec.Input)
args := codexExecArgs(profile.Args, sess.externalID, prompt)
output, err := c.executeCommand(ctx, spec, profile, args, prompt, sink)
if err != nil {
return err
}
if externalID := parseCodexSessionID(output); externalID != "" {
sess.externalID = externalID
}
if sess.externalID == "" {
return fmt.Errorf("cli adapter: codex exec did not report a session id")
}
return nil
}
func (c *CLI) resolveCodexExecSession(spec runtime.ExecutionSpec) (*codexExecSession, error) {
key := sessionKey{model: spec.Model, sessionID: normalizeSessionID(spec.SessionID)}
c.mu.Lock()
defer c.mu.Unlock()
if sess, ok := c.codexSessions[key]; ok {
return sess, nil
}
if spec.SessionMode == runtime.SessionModeRequireExisting {
return nil, fmt.Errorf("cli adapter: no persistent session for profile %q session %q", spec.Model, key.sessionID)
}
sess := &codexExecSession{key: key}
c.codexSessions[key] = sess
return sess, nil
}
func codexExecArgs(configured []string, externalSessionID, prompt string) []string {
if externalSessionID == "" {
args := append([]string{}, configured...)
return append(args, prompt)
}
args := []string{"exec", "resume"}
args = append(args, codexResumeOptions(configured)...)
args = append(args, externalSessionID, prompt)
return args
}
func codexResumeOptions(configured []string) []string {
if len(configured) > 0 && configured[0] == "exec" {
configured = configured[1:]
}
out := make([]string, 0, len(configured))
for i := 0; i < len(configured); i++ {
arg := configured[i]
switch {
case arg == "--color" || arg == "-C" || arg == "--cd" || arg == "-s" || arg == "--sandbox" ||
arg == "--profile" || arg == "-p" || arg == "--local-provider" || arg == "--add-dir" ||
arg == "--output-schema":
if i+1 < len(configured) {
i++
}
continue
case strings.HasPrefix(arg, "--color=") || strings.HasPrefix(arg, "--cd=") ||
strings.HasPrefix(arg, "--sandbox=") || strings.HasPrefix(arg, "--profile=") ||
strings.HasPrefix(arg, "--local-provider=") || strings.HasPrefix(arg, "--add-dir=") ||
strings.HasPrefix(arg, "--output-schema="):
continue
default:
out = append(out, arg)
}
}
return out
}
func parseCodexSessionID(output string) string {
if matches := codexSessionIDPattern.FindStringSubmatch(output); len(matches) >= 2 {
return matches[1]
}
for _, raw := range codexThreadStartedRegex.FindAllString(output, -1) {
var ev struct {
Type string `json:"type"`
ThreadID string `json:"thread_id"`
}
if err := json.Unmarshal([]byte(raw), &ev); err == nil && ev.Type == "thread.started" && ev.ThreadID != "" {
return ev.ThreadID
}
}
return ""
}