120 lines
3.2 KiB
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 ""
|
|
}
|