diff --git a/action.yml b/action.yml index 6aec2ff..9bcfe2e 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,10 @@ inputs: description: 'Session inactivity timeout in seconds — auto-stops after this duration of inactivity' required: false default: '0' + stop-on-run-finished: + description: 'Stop the session automatically when the agent finishes its run' + required: false + default: 'false' model: description: 'Model override (e.g. claude-sonnet-4-20250514)' required: false @@ -75,6 +79,9 @@ outputs: session-uid: description: 'Created session UID' value: ${{ steps.run.outputs.session-uid }} + session-url: + description: 'URL to the session in the Ambient UI' + value: ${{ steps.run.outputs.session-url }} session-phase: description: 'Final session phase (only set when wait is true)' value: ${{ steps.run.outputs.session-phase }} @@ -102,6 +109,7 @@ runs: INPUT_LABELS: ${{ inputs.labels }} INPUT_ENV_VARS: ${{ inputs.environment-variables }} INPUT_TIMEOUT: ${{ inputs.timeout }} + INPUT_STOP_ON_RUN_FINISHED: ${{ inputs.stop-on-run-finished }} INPUT_MODEL: ${{ inputs.model }} INPUT_WORKFLOW: ${{ inputs.workflow }} INPUT_WAIT: ${{ inputs.wait }} @@ -149,8 +157,14 @@ runs: if [ "$INPUT_WAIT" = "true" ]; then ARGS+=(--wait) fi + if [ "$INPUT_STOP_ON_RUN_FINISHED" = "true" ]; then + ARGS+=(--stop-on-run-finished) + fi if [ -n "$INPUT_SESSION_NAME" ]; then ARGS+=(--session-name "$INPUT_SESSION_NAME") + # Pre-set session-url for reuse case (output file may not have it) + BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||') + echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${INPUT_SESSION_NAME}" >> "$GITHUB_OUTPUT" fi if [ "$INPUT_NO_VERIFY_SSL" = "true" ]; then ARGS+=(--no-verify-ssl) @@ -167,6 +181,15 @@ runs: echo "session-name=$SESSION_NAME" >> "$GITHUB_OUTPUT" echo "session-uid=$SESSION_UID" >> "$GITHUB_OUTPUT" echo "session-phase=$SESSION_PHASE" >> "$GITHUB_OUTPUT" + + # Construct session URL from API URL + if [ -n "$SESSION_NAME" ]; then + BASE_URL=$(echo "$INPUT_API_URL" | sed 's|/api$||; s|/api/$||') + echo "session-url=${BASE_URL}/projects/${INPUT_PROJECT}/sessions/${SESSION_NAME}" >> "$GITHUB_OUTPUT" + else + echo "session-url=" >> "$GITHUB_OUTPUT" + fi + { echo "session-result<> "$GITHUB_OUTPUT" echo "session-result=" >> "$GITHUB_OUTPUT" fi diff --git a/create_session.py b/create_session.py index 90be2b5..67ec54a 100644 --- a/create_session.py +++ b/create_session.py @@ -172,6 +172,7 @@ def create_session( labels: dict | None = None, env_vars: dict | None = None, timeout: int = 0, + stop_on_run_finished: bool = False, model: str = "", verify_ssl: bool = True, ) -> dict | None: @@ -192,6 +193,8 @@ def create_session( body["environmentVariables"] = env_vars if timeout: body["inactivityTimeout"] = timeout + if stop_on_run_finished: + body["stopOnRunFinished"] = True if model: body["llmSettings"] = {"model": model} @@ -218,6 +221,9 @@ def create_session( return None +AGENT_DONE_STATUSES = {"idle", "waiting_input"} + + def poll_session( api_url: str, api_token: str, @@ -227,7 +233,12 @@ def poll_session( timeout_minutes: int = 30, verify_ssl: bool = True, ) -> dict: - """Poll session status until a terminal phase is reached.""" + """Poll session until the agent is done or session reaches a terminal phase. + + Exits when: + - Session phase is terminal (Completed, Error, Timeout, Stopped, Failed) + - Agent status is idle or waiting_input (agent finished its run, session still alive) + """ url = f"{api_url.rstrip('/')}/projects/{project}/agentic-sessions/{session_name}" headers = {"Authorization": f"Bearer {api_token}"} deadline = time.time() + (timeout_minutes * 60) + 120 @@ -237,6 +248,7 @@ def poll_session( f"(timeout: {timeout_minutes}m + 2m buffer)" ) + seen_working = False while time.time() < deadline: try: resp = requests.get( @@ -247,23 +259,39 @@ def poll_session( status = data.get("status", {}) phase = status.get("phase", "Unknown") + agent_status = status.get("agentStatus", "") - logger.info(f"Session {session_name}: phase={phase}") + logger.info(f"Session {session_name}: phase={phase}, agentStatus={agent_status}") if phase in TERMINAL_PHASES: return { "phase": phase, + "agentStatus": agent_status, "result": status.get("result", ""), "completionTime": status.get("completionTime", ""), } + # Track if the agent has been active at least once + if agent_status and agent_status not in AGENT_DONE_STATUSES: + seen_working = True + + # Only exit on idle/waiting_input after the agent has been working + if seen_working and agent_status in AGENT_DONE_STATUSES: + logger.info(f"Session {session_name}: agent is {agent_status}, done waiting") + return { + "phase": phase, + "agentStatus": agent_status, + "result": status.get("result", ""), + "completionTime": "", + } + except requests.RequestException as e: logger.warning(f"Poll request failed (will retry): {e}") time.sleep(poll_interval) logger.error("Polling timed out waiting for session completion") - return {"phase": "PollTimeout", "result": "", "completionTime": ""} + return {"phase": "PollTimeout", "agentStatus": "", "result": "", "completionTime": ""} def write_output(output_file: str, data: dict) -> None: @@ -294,6 +322,7 @@ def main(): parser.add_argument("--labels", default="") parser.add_argument("--env-vars", default="") parser.add_argument("--timeout", type=int, default=0) + parser.add_argument("--stop-on-run-finished", action="store_true") parser.add_argument("--model", default="") parser.add_argument("--wait", action="store_true") parser.add_argument("--poll-interval", type=int, default=15) @@ -377,6 +406,7 @@ def main(): labels=labels, env_vars=env_vars, timeout=args.timeout, + stop_on_run_finished=args.stop_on_run_finished, model=args.model, verify_ssl=verify_ssl, )