iop/apps/node/internal/adapters/cli/codex_exec.go

120 lines
3.2 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"regexp"
"iop/apps/node/internal/runtime"
"iop/packages/go/config"
)
var (
codexSessionIDPattern = regexp.MustCompile(`(?mi)^session id:\s*(\S+)\s*$`)
codexThreadStartedRegex = regexp.MustCompile(`\{[^{}]*"type"\s*:\s*"thread\.started"[^{}]*\}`)
)
func (e *codexExecutor) Execute(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error {
if profile.ResumeArgs == nil {
return fmt.Errorf("cli adapter: codex-exec mode requires resume_args in profile %q", spec.Target)
}
sess, err := e.resolveCodexExecSession(spec)
if err != nil {
return err
}
sess.mu.Lock()
defer sess.mu.Unlock()
prompt := extractPrompt(spec.Input)
args := codexExecArgs(profile, sess.externalID, prompt)
output, err := e.cli.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 (e *codexExecutor) resolveCodexExecSession(spec runtime.ExecutionSpec) (*codexExecSession, error) {
target := cliTargetName(spec)
key := newSessionKey(spec)
e.mu.Lock()
defer e.mu.Unlock()
if sess, ok := e.sessions[key]; ok {
return sess, nil
}
if spec.SessionMode == runtime.SessionModeRequireExisting {
return nil, fmt.Errorf("cli adapter: no persistent session for target %q session %q", target, key.sessionID)
}
sess := &codexExecSession{key: key}
e.sessions[key] = sess
return sess, nil
}
func (e *codexExecutor) Sessions() []sessionListEntry {
e.mu.Lock()
defer e.mu.Unlock()
snaps := make([]sessionListEntry, 0, len(e.sessions))
for k := range e.sessions {
snaps = append(snaps, sessionListEntry{"codex-exec", k.target, k.sessionID, k.workspace})
}
return snaps
}
// Terminate removes every workspace variant of the given target/sessionID.
func (e *codexExecutor) Terminate(_ context.Context, target, sessionID string) (bool, error) {
sid := normalizeSessionID(sessionID)
e.mu.Lock()
defer e.mu.Unlock()
removed := false
for k := range e.sessions {
if k.target == target && k.sessionID == sid {
delete(e.sessions, k)
removed = true
}
}
return removed, nil
}
func (e *codexExecutor) Stop(ctx context.Context) error {
e.mu.Lock()
e.sessions = make(map[sessionKey]*codexExecSession)
e.mu.Unlock()
return nil
}
func codexExecArgs(profile config.CLIProfileConf, externalSessionID, prompt string) []string {
if externalSessionID == "" {
args := append([]string{}, profile.Args...)
return append(args, prompt)
}
args := append([]string{}, profile.ResumeArgs...)
return append(args, externalSessionID, prompt)
}
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 ""
}