diff --git a/.github/workflows/traiage.yaml b/.github/workflows/traiage.yaml index 50fe47384f..708bc4611c 100644 --- a/.github/workflows/traiage.yaml +++ b/.github/workflows/traiage.yaml @@ -12,6 +12,11 @@ on: required: true default: "traiage" type: string + template_preset: + description: "Template preset to use" + required: true + default: "Default" + type: string prefix: description: "Prefix for workspace name" required: false @@ -68,35 +73,25 @@ jobs: coder whoami echo "$HOME/.local/bin" >> "${GITHUB_PATH}" - - name: Create Coder workspace - id: create-workspace + # TODO(Cian): this is a good use-case for 'recipes' + - name: Create Coder task + id: create-task env: - PREFIX: ${{ inputs.prefix }} CONTEXT_KEY: ${{ steps.extract-context.outputs.context_key }} + GITHUB_TOKEN: ${{ github.token }} + ISSUE_URL: ${{ inputs.issue_url }} + PREFIX: ${{ inputs.prefix }} RUN_ID: ${{ github.run_id }} TEMPLATE_PARAMETERS: ${{ secrets.TRAIAGE_TEMPLATE_PARAMETERS }} + TEMPLATE_PRESET: ${{ inputs.template_preset }} + # TODO: replace with coder exp task command + APP_SLUG: ccw run: | - export WORKSPACE_NAME="${PREFIX}-${CONTEXT_KEY}-${RUN_ID}" - echo "Creating workspace: $WORKSPACE_NAME" - ./scripts/traiage.sh create - echo "workspace_name=$WORKSPACE_NAME" >> "${GITHUB_OUTPUT}" - echo "WORKSPACE_NAME=${WORKSPACE_NAME}" >> "${GITHUB_ENV}" - - - name: Send prompt to AI agent inside workspace - id: prepare-prompt - env: - WORKSPACE_NAME: ${{ steps.create-workspace.outputs.workspace_name }} - ISSUE_URL: ${{ inputs.issue_url }} - GITHUB_TOKEN: ${{ github.token }} - run: | - PROMPT_FILE=$(mktemp) - trap 'rm -f "${PROMPT_FILE}"' EXIT - # Fetch issue description using `gh` CLI issue_description=$(gh issue view "${ISSUE_URL}") # Write a prompt to PROMPT_FILE - cat > "${PROMPT_FILE}" <> "${GITHUB_OUTPUT}" + echo "TASK_NAME=${TASK_NAME}" >> "${GITHUB_ENV}" - name: Create and upload archive id: create-archive env: BUCKET_PREFIX: "gs://coder-traiage-outputs/traiage" run: | - echo "Creating archive for workspace: $WORKSPACE_NAME" + echo "Creating archive for workspace: $TASK_NAME" ./scripts/traiage.sh archive - echo "archive_url=${BUCKET_PREFIX%%/}/$WORKSPACE_NAME.tar.gz" >> "${GITHUB_OUTPUT}" + echo "archive_url=${BUCKET_PREFIX%%/}/$TASK_NAME.tar.gz" >> "${GITHUB_OUTPUT}" - name: Generate a summary of the changes and post a comment on GitHub. id: generate-summary env: ARCHIVE_URL: ${{ steps.create-archive.outputs.archive_url }} + APP_SLUG: ccw BUCKET_PREFIX: "gs://coder-traiage-outputs/traiage" CONTEXT_KEY: ${{ steps.extract-context.outputs.context_key }} GITHUB_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} ISSUE_URL: ${{ inputs.issue_url }} - WORKSPACE_NAME: ${{ steps.create-workspace.outputs.workspace_name }} + TASK_NAME: ${{ steps.create-task.outputs.TASK_NAME }} run: | SUMMARY_FILE=$(mktemp) trap 'rm -f "${SUMMARY_FILE}"' EXIT @@ -137,7 +137,7 @@ jobs: echo "## TrAIage Results" echo "- **Issue URL:** ${ISSUE_URL}" echo "- **Context Key:** ${CONTEXT_KEY}" - echo "- **Workspace:** ${WORKSPACE_NAME}" + echo "- **Workspace:** ${TASK_NAME}" echo "- **Archive URL:** ${ARCHIVE_URL}" echo echo "${AUTO_SUMMARY}" @@ -145,7 +145,7 @@ jobs: echo "To fetch the output to your own workspace:" echo echo '```bash' - echo "BUCKET_PREFIX=${BUCKET_PREFIX} WORKSPACE_NAME=${WORKSPACE_NAME} ./scripts/traiage.sh resume" + echo "BUCKET_PREFIX=${BUCKET_PREFIX} TASK_NAME=${TASK_NAME} ./scripts/traiage.sh resume" echo '```' echo } >> "${SUMMARY_FILE}" @@ -157,8 +157,8 @@ jobs: fi cat "${SUMMARY_FILE}" >> "${GITHUB_STEP_SUMMARY}" - - name: Cleanup workspace - if: steps.create-workspace.outputs.workspace_name != '' && steps.create-archive.outputs.archive_url != '' + - name: Cleanup task + if: steps.create-task.outputs.TASK_NAME != '' && steps.create-archive.outputs.archive_url != '' run: | - echo "Cleaning up workspace: $WORKSPACE_NAME" + echo "Cleaning up task: $TASK_NAME" ./scripts/traiage.sh delete || true diff --git a/scripts/traiage.sh b/scripts/traiage.sh index a94ca25794..78639885ed 100755 --- a/scripts/traiage.sh +++ b/scripts/traiage.sh @@ -5,7 +5,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") source "${SCRIPT_DIR}/lib.sh" CODER_BIN=${CODER_BIN:-"$(which coder)"} -AGENTAPI_SLUG=${AGENTAPI_SLUG:-""} +APP_SLUG=${APP_SLUG:-""} TEMPDIR=$(mktemp -d) trap 'rm -rf "${TEMPDIR}"' EXIT @@ -19,41 +19,36 @@ usage() { } create() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME TEMPLATE_NAME TEMPLATE_PARAMETERS - # Check if a workspace already exists - exists=$("${CODER_BIN}" \ + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME TEMPLATE_NAME TEMPLATE_PRESET PROMPT + # Check if a task already exists + set +e + task_json=$("${CODER_BIN}" \ --url "${CODER_URL}" \ --token "${CODER_SESSION_TOKEN}" \ - list \ - --search "owner:me" \ - --output json | - jq -r --arg name "${WORKSPACE_NAME}" 'any(.[]; select(.name == $name))') - if [[ "${exists}" == "true" ]]; then - echo "Workspace ${WORKSPACE_NAME} already exists." + exp tasks status "${TASK_NAME}" \ + --output json) + set -e + + if [[ "${TASK_NAME}" == $(jq -r '.name' <<<"${task_json}") ]]; then + # TODO(Cian): Send PROMPT to the agent in the existing workspace. + echo "Task \"${TASK_NAME}\" already exists." exit 0 fi + "${CODER_BIN}" \ --url "${CODER_URL}" \ --token "${CODER_SESSION_TOKEN}" \ - create \ + exp tasks create \ + --name "${TASK_NAME}" \ --template "${TEMPLATE_NAME}" \ - --stop-after 30m \ - --parameter "${TEMPLATE_PARAMETERS}" \ - --yes \ - "${WORKSPACE_NAME}" + --preset "${TEMPLATE_PRESET}" \ + --org coder \ + --stdin <<<"${PROMPT}" exit 0 } -prompt() { - if [[ -z "${AGENTAPI_SLUG}" ]]; then - prompt_ssh - else - prompt_agentapi - fi -} - ssh_config() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME if [[ -n "${OPENSSH_CONFIG_FILE:-}" ]]; then echo "Using existing SSH config file: ${OPENSSH_CONFIG_FILE}" @@ -71,24 +66,8 @@ ssh_config() { export OPENSSH_CONFIG_FILE } -prompt_ssh() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME PROMPT - - ssh_config - - # Execute claude over SSH and provide prompt via stdin - # Note: use of cat to work around claude-code#7357 - ssh \ - -F "${OPENSSH_CONFIG_FILE}" \ - "${WORKSPACE_NAME}.coder" \ - -- \ - "cat | \"\${HOME}\"/.local/bin/claude --dangerously-skip-permissions --print --verbose --output-format=stream-json" \ - <<<"${PROMPT}" - exit 0 -} - -prompt_agentapi() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME AGENTAPI_SLUG PROMPT +prompt() { + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME APP_SLUG PROMPT wait_agentapi_stable @@ -114,17 +93,35 @@ prompt_agentapi() { --request POST \ --show-error \ --silent \ - "${CODER_URL}/@${username}/${WORKSPACE_NAME}/apps/${AGENTAPI_SLUG}/message" | jq -r '.ok') + "${CODER_URL}/@${username}/${TASK_NAME}/apps/${APP_SLUG}/message" | jq -r '.ok') if [[ "${response}" != "true" ]]; then echo "Failed to send prompt" exit 1 fi + # Wait for agentapi to process the response and return the last agent message wait_agentapi_stable + + CODER_USERNAME="${username}" last_message +} + +last_message() { + requiredenvs CODER_URL CODER_SESSION_TOKEN CODER_USERNAME TASK_NAME APP_SLUG PROMPT + last_msg=$(curl \ + --fail \ + --header "Content-Type: application/json" \ + --header "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \ + --location \ + --show-error \ + --silent \ + "${CODER_URL}/@${CODER_USERNAME}/${TASK_NAME}/apps/${APP_SLUG}/messages" | + jq -r 'last(.messages[] | select(.role=="agent") | [.])') + + echo "${last_msg}" } wait_agentapi_stable() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME PROMPT + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME APP_SLUG username=$(curl \ --fail \ --header "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \ @@ -134,33 +131,140 @@ wait_agentapi_stable() { "${CODER_URL}/api/v2/users/me" | jq -r '.username') for attempt in {1..120}; do - response=$(curl \ - --fail \ + # First wait for task to start + set +o pipefail + task_status=$("${CODER_BIN}" \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --output json | + jq -r '.status') + set -o pipefail + echo "Task status is ${task_status}" + if [[ "${task_status}" == "running" ]]; then + echo "Task is running" + break + fi + echo "Waiting for task status to be running (attempt ${attempt}/120)" + sleep 5 + done + + for attempt in {1..120}; do + # Workspace agent must be healthy + set +o pipefail + healthy=$("${CODER_BIN}" \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --output json | + jq -r '.workspace_agent_health.healthy') + set -o pipefail + if [[ "${healthy}" == "true" ]]; then + echo "Workspace agent is healthy" + break + fi + echo "Workspace agent not yet healthy (attempt ${attempt}/120)" + sleep 5 + done + + for attempt in {1..120}; do + # AgentAPI application should not be 404'ing + agentapi_app_status_code=$(curl \ --header "Content-Type: application/json" \ --header "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \ --location \ --request GET \ --show-error \ --silent \ - "${CODER_URL}/@${username}/${WORKSPACE_NAME}/apps/agentapi/status" | jq -r '.status') - if [[ "${response}" == "stable" ]]; then + --output /dev/null \ + --write-out '%{http_code}' \ + "${CODER_URL}/@${username}/${TASK_NAME}/apps/${APP_SLUG}/status") + echo "Workspace app ${APP_SLUG} returned ${agentapi_app_status_code}" + if [[ "${agentapi_app_status_code}" == "200" ]]; then + echo "AgentAPI is running" + break + fi + echo "AgentAPI not yet running (attempt ${attempt}/120)" + sleep 5 + done + + for attempt in {1..120}; do + # AgentAPI must be stable + set +o pipefail + agentapi_status=$(curl \ + --header "Content-Type: application/json" \ + --header "Coder-Session-Token: ${CODER_SESSION_TOKEN}" \ + --location \ + --request GET \ + --show-error \ + --silent \ + "${CODER_URL}/@${username}/${TASK_NAME}/apps/${APP_SLUG}/status" | jq -r '.status') + set -o pipefail + if [[ "${agentapi_status}" == "stable" ]]; then echo "AgentAPI stable" break fi echo "Waiting for AgentAPI to report stable status (attempt ${attempt}/120)" sleep 5 done + + # At this point the task is running, the workspace agent is healthy, + # the AgentAPI app is running and the AgentAPI reports stable status. + # Now we wait for the Task Agent to report 'idle', 'complete', or 'failure'. + for attempt in {1..120}; do + task_json=$(${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --output json) + set +o pipefail + current_state=$(jq -r '.current_state.state' <<<"${task_json}") + current_message=$(jq -r '.current_state.message' <<<"${task_json}") + set -o pipefail + + # The current_state or current_message may be null if the Task Agent + # has not yet reported its status. + if [[ -z "${current_state}" ]] || [[ -z "${current_message}" ]]; then + echo "Waiting for Task Agent to report state (attempt ${attempt}/120)" + sleep 5 + continue + fi + break + done + + # Finally, wait for the Task Agent to report 'idle', 'complete', or 'failure'. This is unbounded, as we don't know how long the agent will take. + while true; do + task_json=$(${CODER_BIN} \ + --url "${CODER_URL}" \ + --token "${CODER_SESSION_TOKEN}" \ + exp tasks status "${TASK_NAME}" \ + --output json) + set +o pipefail + + current_state=$(jq -r '.current_state.state' <<<"${task_json}") + current_message=$(jq -r '.current_state.message' <<<"${task_json}") + set -o pipefail + if [[ "${current_state}" == "working" ]]; then + echo "Task Agent is ${current_state} and reports: \"${current_message}\"" + echo "Waiting for Task Agent to report idle state (attempt ${attempt}/120)" + sleep 5 + continue + else + echo "Task Agent is ${current_state} and reports: \"${current_message}\"" + break + fi + done } archive() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME BUCKET_PREFIX + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME BUCKET_PREFIX ssh_config # We want the heredoc to be expanded locally and not remotely. # shellcheck disable=SC2087 ARCHIVE_DEST=$( ssh -F "${OPENSSH_CONFIG_FILE}" \ - "${WORKSPACE_NAME}.coder" \ + "${TASK_NAME}.coder" \ bash <<-EOF #!/usr/bin/env bash set -euo pipefail @@ -183,14 +287,14 @@ archive() { } summary() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME APP_SLUG ssh_config # We want the heredoc to be expanded locally and not remotely. # shellcheck disable=SC2087 ssh \ -F "${OPENSSH_CONFIG_FILE}" \ - "${WORKSPACE_NAME}.coder" \ + "${TASK_NAME}.coder" \ -- \ bash <<-EOF #!/usr/bin/env bash @@ -211,19 +315,19 @@ summary() { } commit_push() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME ssh_config # We want the heredoc to be expanded locally and not remotely. # shellcheck disable=SC2087 ssh \ -F "${OPENSSH_CONFIG_FILE}" \ - "${WORKSPACE_NAME}.coder" \ + "${TASK_NAME}.coder" \ -- \ bash <<-EOF #!/usr/bin/env bash set -euo pipefail - BRANCH="traiage/${WORKSPACE_NAME}" + BRANCH="traiage/${TASK_NAME}" if [[ \$(git branch --show-current) != "\${BRANCH}" ]]; then git checkout -b "\${BRANCH}" fi @@ -245,36 +349,37 @@ commit_push() { exit $? } +# TODO(Cian): Update this to delete the task when available. delete() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME "${CODER_BIN}" \ --url "${CODER_URL}" \ --token "${CODER_SESSION_TOKEN}" \ delete \ - "${WORKSPACE_NAME}" \ + "${TASK_NAME}" \ --yes exit 0 } resume() { - requiredenvs CODER_URL CODER_SESSION_TOKEN WORKSPACE_NAME BUCKET_PREFIX + requiredenvs CODER_URL CODER_SESSION_TOKEN TASK_NAME BUCKET_PREFIX - # Note: WORKSPACE_NAME here is really the 'context key'. + # Note: TASK_NAME here is really the 'context key'. # Files are uploaded to the GCS bucket under this key. # This just happens to be the same as the workspace name. - src="${BUCKET_PREFIX%%/}/${WORKSPACE_NAME}.tar.gz" - dest="${TEMPDIR}/${WORKSPACE_NAME}.tar.gz" + src="${BUCKET_PREFIX%%/}/${TASK_NAME}.tar.gz" + dest="${TEMPDIR}/${TASK_NAME}.tar.gz" gcloud storage cp "${src}" "${dest}" if [[ ! -f "${dest}" ]]; then echo "FATAL: Failed to download archive from ${src}" exit 1 fi - resume_dest="${HOME}/workspaces/${WORKSPACE_NAME}" + resume_dest="${HOME}/tasks/${TASK_NAME}" mkdir -p "${resume_dest}" tar -xzvf "${dest}" -C "${resume_dest}" || exit 1 - echo "Workspace restored to ${resume_dest}" + echo "Task context restored to ${resume_dest}" } main() { @@ -300,7 +405,7 @@ main() { delete) delete ;; - wait-agentapi-stable) + wait) wait_agentapi_stable ;; resume)