-
Notifications
You must be signed in to change notification settings - Fork 0
fix(agy): pure SpawnArgs, no temp-file leak, forward signals to agy #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,8 +2,6 @@ package agy | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||
| "sync/atomic" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -21,21 +19,46 @@ const ( | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultBinary = "agy" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // validConversationID matches the UUID format used by agy conversation IDs. | ||||||||||||||||||||||||||||||||||||||||||||||||
| var validConversationID = regexp.MustCompile( | ||||||||||||||||||||||||||||||||||||||||||||||||
| `Created conversation ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // shellWrapper is the sh -c script that runs agy (via $0) and emits a JSON | ||||||||||||||||||||||||||||||||||||||||||||||||
| // MessageResult sentinel on clean exit. Using "$0" instead of embedding the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // binary path in the script prevents shell injection via WithBinary. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const shellWrapper = `"$0" "$@"; _E=$?; [ $_E -eq 0 ] && printf '{"type":"result","stop_reason":"end_turn"}\n'; exit $_E` | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Backend is an Antigravity CLI backend for agentrun. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // It implements cli.Spawner, cli.Parser, and cli.Resumer for a spawn-per-turn model. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // shellWrapper is the sh -c script that runs agy and adapts it to the agentrun | ||||||||||||||||||||||||||||||||||||||||||||||||
| // CLI engine. agy is plain-text and records the conversation ID only in its log | ||||||||||||||||||||||||||||||||||||||||||||||||
| // file, so the wrapper: | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||
| // - allocates its OWN temp log via mktemp (so SpawnArgs stays a pure, I/O-free | ||||||||||||||||||||||||||||||||||||||||||||||||
| // argument builder and nothing leaks on Engine.Validate or single-turn runs), | ||||||||||||||||||||||||||||||||||||||||||||||||
| // - runs agy in the background and forwards SIGTERM/SIGINT to it, so the engine | ||||||||||||||||||||||||||||||||||||||||||||||||
| // signalling sh on Stop/replacement actually terminates agy instead of | ||||||||||||||||||||||||||||||||||||||||||||||||
| // orphaning it, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // - on clean exit, extracts "Created conversation <id>" from the log and emits | ||||||||||||||||||||||||||||||||||||||||||||||||
| // it as an {"type":"agy_session",...} sentinel for the parser to capture (so | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ResumeArgs needs no file side channel), followed by the MessageResult | ||||||||||||||||||||||||||||||||||||||||||||||||
| // sentinel, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // - always removes its temp log and preserves agy's exit code. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||
| // The binary is passed as argv[0] ($0), never interpolated into the script, so | ||||||||||||||||||||||||||||||||||||||||||||||||
| // WithBinary cannot inject shell metacharacters. | ||||||||||||||||||||||||||||||||||||||||||||||||
| const shellWrapper = `__log=$(mktemp 2>/dev/null) || __log= | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$__log" ]; then set -- --log-file "$__log" "$@"; fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| "$0" "$@" & | ||||||||||||||||||||||||||||||||||||||||||||||||
| __pid=$! | ||||||||||||||||||||||||||||||||||||||||||||||||
| trap 'kill -TERM "$__pid" 2>/dev/null' TERM INT | ||||||||||||||||||||||||||||||||||||||||||||||||
| wait "$__pid" | ||||||||||||||||||||||||||||||||||||||||||||||||
| __e=$? | ||||||||||||||||||||||||||||||||||||||||||||||||
| trap - TERM INT | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [ "$__e" -eq 0 ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$__log" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| __cid=$(sed -n 's/.*Created conversation \([0-9a-f-][0-9a-f-]*\).*/\1/p' "$__log" 2>/dev/null | head -n 1) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$__cid" ]; then printf '{"type":"agy_session","id":"%s"}\n' "$__cid"; fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| printf '{"type":"result","stop_reason":"end_turn"}\n' | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "$__log" ]; then rm -f "$__log"; fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit "$__e"` | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Backend is an Antigravity CLI backend for agentrun. It implements cli.Spawner, | ||||||||||||||||||||||||||||||||||||||||||||||||
| // cli.Parser, and cli.Resumer for a spawn-per-turn model. The conversation ID is | ||||||||||||||||||||||||||||||||||||||||||||||||
| // captured (write-once) from the wrapper's agy_session sentinel by ParseLine. | ||||||||||||||||||||||||||||||||||||||||||||||||
| type Backend struct { | ||||||||||||||||||||||||||||||||||||||||||||||||
| binary string | ||||||||||||||||||||||||||||||||||||||||||||||||
| logFile string // path to the temporary log file for capturing the session ID | ||||||||||||||||||||||||||||||||||||||||||||||||
| resumeID atomic.Pointer[string] | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,9 +92,8 @@ func New(opts ...Option) *Backend { | |||||||||||||||||||||||||||||||||||||||||||||||
| return b | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // buildWrapperArgs wraps agyArgs + prompt in a sh -c invocation that emits | ||||||||||||||||||||||||||||||||||||||||||||||||
| // the MessageResult sentinel on success. The binary is passed as argv[0] ($0) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // to avoid shell injection via binary path metacharacters. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // buildWrapperArgs wraps agyArgs + prompt in the sh -c invocation. The binary is | ||||||||||||||||||||||||||||||||||||||||||||||||
| // passed as argv[0] ($0) to avoid shell injection via binary path metacharacters. | ||||||||||||||||||||||||||||||||||||||||||||||||
| func (b *Backend) buildWrapperArgs(agyArgs []string, prompt string) (string, []string) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| args := make([]string, 0, len(agyArgs)+5) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // argv[0] = b.binary (becomes $0 in the script); remaining args become $@. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -81,20 +103,11 @@ func (b *Backend) buildWrapperArgs(agyArgs []string, prompt string) (string, []s | |||||||||||||||||||||||||||||||||||||||||||||||
| return "sh", args | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // SpawnArgs builds exec.Cmd arguments for a new agy session. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // SpawnArgs builds exec.Cmd arguments for a new agy session. It is a pure | ||||||||||||||||||||||||||||||||||||||||||||||||
| // argument builder with no side effects: the temporary log file is allocated by | ||||||||||||||||||||||||||||||||||||||||||||||||
| // the shell wrapper at run time, not here (Engine.Validate also calls SpawnArgs). | ||||||||||||||||||||||||||||||||||||||||||||||||
| func (b *Backend) SpawnArgs(session agentrun.Session) (string, []string) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Create a temp log file so agy records the new conversation ID. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // If creation fails we omit --log-file; ResumeArgs falls back to OptionResumeID. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if f, err := os.CreateTemp("", "agy-*.log"); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||
| f.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||
| b.logFile = f.Name() | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| var agyArgs []string | ||||||||||||||||||||||||||||||||||||||||||||||||
| if b.logFile != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||
| agyArgs = append(agyArgs, "--log-file", b.logFile) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| agyArgs = appendSessionArgs(agyArgs, session) | ||||||||||||||||||||||||||||||||||||||||||||||||
| agyArgs := appendSessionArgs(nil, session) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
109
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| prompt := session.Prompt | ||||||||||||||||||||||||||||||||||||||||||||||||
| if jsonutil.ContainsNull(prompt) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -104,7 +117,8 @@ func (b *Backend) SpawnArgs(session agentrun.Session) (string, []string) { | |||||||||||||||||||||||||||||||||||||||||||||||
| return b.buildWrapperArgs(agyArgs, prompt) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // ResumeArgs builds exec.Cmd arguments to resume an existing agy session. | ||||||||||||||||||||||||||||||||||||||||||||||||
| // ResumeArgs builds exec.Cmd arguments to resume an existing agy session, using | ||||||||||||||||||||||||||||||||||||||||||||||||
| // the conversation ID captured by ParseLine (or OptionResumeID as a fallback). | ||||||||||||||||||||||||||||||||||||||||||||||||
| func (b *Backend) ResumeArgs(session agentrun.Session, initialPrompt string) (string, []string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if jsonutil.ContainsNull(initialPrompt) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return "", nil, errors.New("agy: initial prompt contains null bytes") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,31 +127,9 @@ func (b *Backend) ResumeArgs(session agentrun.Session, initialPrompt string) (st | |||||||||||||||||||||||||||||||||||||||||||||||
| return "", nil, err | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Determine the conversation UUID to resume. | ||||||||||||||||||||||||||||||||||||||||||||||||
| var uuid string | ||||||||||||||||||||||||||||||||||||||||||||||||
| if captured := b.resumeID.Load(); captured != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||
| uuid = *captured | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if uuid == "" && b.logFile != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // First resume: read the log file written by SpawnArgs to get the UUID. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if data, err := os.ReadFile(b.logFile); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if m := validConversationID.FindSubmatch(data); len(m) == 2 { | ||||||||||||||||||||||||||||||||||||||||||||||||
| uuid = string(m[1]) | ||||||||||||||||||||||||||||||||||||||||||||||||
| b.resumeID.Store(&uuid) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| _ = os.Remove(b.logFile) | ||||||||||||||||||||||||||||||||||||||||||||||||
| b.logFile = "" | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Fallback to explicitly-provided resume ID. | ||||||||||||||||||||||||||||||||||||||||||||||||
| uuid := b.resolveResumeID(session) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if uuid == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||
| uuid = session.Options[agentrun.OptionResumeID] | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if uuid == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return "", nil, errors.New("agy: no conversation ID available (not captured from log and OptionResumeID not set)") | ||||||||||||||||||||||||||||||||||||||||||||||||
| return "", nil, errors.New("agy: no conversation ID available (not captured from output and OptionResumeID not set)") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a caller sends a replacement message while the first agy turn is still running, Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| agyArgs := []string{"--conversation", uuid} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -147,6 +139,15 @@ func (b *Backend) ResumeArgs(session agentrun.Session, initialPrompt string) (st | |||||||||||||||||||||||||||||||||||||||||||||||
| return binary, args, nil | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // resolveResumeID returns the auto-captured conversation ID (write-once from the | ||||||||||||||||||||||||||||||||||||||||||||||||
| // agy_session sentinel) or the explicit OptionResumeID fallback. | ||||||||||||||||||||||||||||||||||||||||||||||||
| func (b *Backend) resolveResumeID(session agentrun.Session) string { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if p := b.resumeID.Load(); p != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return *p | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| return session.Options[agentrun.OptionResumeID] | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| func appendSessionArgs(args []string, session agentrun.Session) []string { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if session.Model != "" && !jsonutil.ContainsNull(session.Model) && !strings.HasPrefix(session.Model, "-") { | ||||||||||||||||||||||||||||||||||||||||||||||||
| args = append(args, "--model", session.Model) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trap command
kill -TERM "$__pid"is asynchronous. When the shell wrapper receivesSIGTERMorSIGINT, it sends the signal toagyand immediately proceeds to delete the log file and exit, without waiting foragyto actually terminate. This can leaveagyrunning as an orphaned process and causes a race condition where the log file is deleted whileagyis still shutting down. Waiting for the process to exit inside the trap ensures a clean and graceful shutdown.