mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 09:15:22 -04:00
3d1a9a9450
The Deep Agents Code GitHub Action now supports headless options for model configuration, skills, startup commands, sandboxes, MCP, interpreter tools, turn and task limits, rubric grading, stdin, quiet output, buffered output, and JSON output. --- The root GitHub Action exposed only a small subset of headless `dcode` options, so workflows could not configure skills, sandboxes, MCP, interpreter tools, rubric grading, model parameters, or output modes without bypassing the Action. This expands the Action input surface to match supported headless `dcode` behavior while deliberately keeping interactive-only `--auto-approve` out of workflows. Inputs are assembled as shell argument arrays, typed values are validated before launch, stdin and timeout handling preserve the agent's actual exit code, and memory caching now includes SQLite WAL files. `ACTION.md` documents the common inputs, outputs, memory behavior, and the need to treat agent output as untrusted. The Action tests compare forwarded flags with the CLI parser and execute the real command-assembly shell against stubbed processes, covering validation, timeout arithmetic, stdin behavior, and exit-code propagation.
540 lines
20 KiB
YAML
540 lines
20 KiB
YAML
name: "Deep Agents Code"
|
|
description: "Run dcode in GitHub workflows"
|
|
author: "LangChain"
|
|
branding:
|
|
icon: "cpu"
|
|
color: "blue"
|
|
|
|
inputs:
|
|
prompt:
|
|
description: "The prompt/instruction to send to the agent"
|
|
required: true
|
|
model:
|
|
description: "Model to use, as provider:model (e.g. openai:gpt-5.5, anthropic:claude-opus-4-6) or a bare name (claude-*, gpt-*, gemini-*) with the provider auto-detected."
|
|
required: false
|
|
model_params:
|
|
description: "JSON model kwargs passed to --model-params"
|
|
required: false
|
|
default: ""
|
|
max_retries:
|
|
description: "Maximum transient model retries passed to --max-retries"
|
|
required: false
|
|
default: ""
|
|
profile_override:
|
|
description: "JSON model profile overrides passed to --profile-override"
|
|
required: false
|
|
default: ""
|
|
anthropic_api_key:
|
|
description: "Anthropic API key"
|
|
required: false
|
|
openai_api_key:
|
|
description: "OpenAI API key"
|
|
required: false
|
|
google_api_key:
|
|
description: "Google API key"
|
|
required: false
|
|
github_token:
|
|
description: "GitHub token for API access"
|
|
required: false
|
|
default: ${{ github.token }}
|
|
working_directory:
|
|
description: "Working directory for the agent"
|
|
required: false
|
|
default: "."
|
|
cli_version:
|
|
description: "deepagents-code version (empty = latest)"
|
|
required: false
|
|
default: ""
|
|
skills_repo:
|
|
description: "GitHub repo of skills to clone (e.g. owner/repo, owner/repo@ref, or full URL)"
|
|
required: false
|
|
default: ""
|
|
enable_memory:
|
|
description: "Persist agent memory across workflow runs using actions/cache. When enabled, memory is keyed by agent_name + memory_scope so the agent can recall prior context."
|
|
required: false
|
|
default: "true"
|
|
memory_scope:
|
|
description: "Cache scope: pr (shared per PR), branch (shared per branch), repo (shared across repo)"
|
|
required: false
|
|
default: "repo"
|
|
agent_name:
|
|
description: "Agent identity name — controls memory namespace (default: agent)"
|
|
required: false
|
|
default: "agent"
|
|
shell_allow_list:
|
|
description: "Comma-separated shell allow list passed to --shell-allow-list (default: recommended,git,gh)"
|
|
required: false
|
|
default: "recommended,git,gh"
|
|
skill:
|
|
description: "Skill name to invoke before running the prompt"
|
|
required: false
|
|
default: ""
|
|
startup_cmd:
|
|
description: "Shell command to run at startup before the prompt"
|
|
required: false
|
|
default: ""
|
|
sandbox:
|
|
description: "Sandbox provider passed to --sandbox (empty = local execution)"
|
|
required: false
|
|
default: ""
|
|
sandbox_id:
|
|
description: "Existing sandbox ID passed to --sandbox-id"
|
|
required: false
|
|
default: ""
|
|
sandbox_snapshot_name:
|
|
description: "Snapshot or blueprint name passed to --sandbox-snapshot-name"
|
|
required: false
|
|
default: ""
|
|
sandbox_setup:
|
|
description: "Setup script path passed to --sandbox-setup"
|
|
required: false
|
|
default: ""
|
|
mcp_config:
|
|
description: "MCP config path passed to --mcp-config"
|
|
required: false
|
|
default: ""
|
|
no_mcp:
|
|
description: "Pass --no-mcp to disable MCP loading"
|
|
required: false
|
|
default: "false"
|
|
trust_project_mcp:
|
|
description: "Pass --trust-project-mcp to skip project MCP approval"
|
|
required: false
|
|
default: "false"
|
|
interpreter:
|
|
description: "Set to true for --interpreter, false for --no-interpreter, empty for dcode default"
|
|
required: false
|
|
default: ""
|
|
interpreter_tools:
|
|
description: "PTC allowlist passed to --interpreter-tools"
|
|
required: false
|
|
default: ""
|
|
max_turns:
|
|
description: "Maximum agentic turns passed to --max-turns"
|
|
required: false
|
|
default: ""
|
|
task_timeout:
|
|
description: "dcode task timeout in seconds passed to --timeout"
|
|
required: false
|
|
default: ""
|
|
rubric:
|
|
description: "Acceptance criteria passed to --rubric (literal text or @path)"
|
|
required: false
|
|
default: ""
|
|
rubric_model:
|
|
description: "Model passed to --rubric-model"
|
|
required: false
|
|
default: ""
|
|
rubric_max_iterations:
|
|
description: "Maximum rubric grader iterations passed to --rubric-max-iterations"
|
|
required: false
|
|
default: ""
|
|
quiet:
|
|
description: "Pass --quiet for clean stdout"
|
|
required: false
|
|
default: "false"
|
|
no_stream:
|
|
description: "Pass --no-stream to buffer the full response"
|
|
required: false
|
|
default: "false"
|
|
json:
|
|
description: "Pass --json for machine-readable dcode output"
|
|
required: false
|
|
default: "false"
|
|
stdin:
|
|
description: "Pass --stdin and pipe prompt as standard input instead of using --non-interactive"
|
|
required: false
|
|
default: "false"
|
|
timeout:
|
|
description: "Maximum action runtime in minutes (default: 30)"
|
|
required: false
|
|
default: "30"
|
|
|
|
outputs:
|
|
response:
|
|
description: "Full text response from the agent"
|
|
value: ${{ steps.run-agent.outputs.response }}
|
|
exit_code:
|
|
description: "Exit code from the agent"
|
|
value: ${{ steps.run-agent.outputs.exit_code }}
|
|
cache_hit:
|
|
description: "Whether agent memory was restored from cache (empty if enable_memory is false)"
|
|
value: ${{ steps.restore-memory.outputs.cache-hit }}
|
|
|
|
runs:
|
|
using: "composite"
|
|
steps:
|
|
- name: Set up uv
|
|
uses: astral-sh/setup-uv@0ca8f610542aa7f4acaf39e65cf4eb3c35091883 # v7
|
|
with:
|
|
enable-cache: true
|
|
|
|
- name: Resolve cache key
|
|
if: inputs.enable_memory == 'true'
|
|
id: cache-key
|
|
shell: bash
|
|
env:
|
|
INPUT_MEMORY_SCOPE: ${{ inputs.memory_scope }}
|
|
INPUT_AGENT_NAME: ${{ inputs.agent_name }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
EVENT_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || '' }}
|
|
run: |
|
|
PREFIX="deepagents-memory-${INPUT_AGENT_NAME}"
|
|
case "$INPUT_MEMORY_SCOPE" in
|
|
pr)
|
|
if [ -n "$EVENT_PR_NUMBER" ]; then
|
|
SCOPE_KEY="pr-${EVENT_PR_NUMBER}"
|
|
else
|
|
SCOPE_KEY="ref-${REF_NAME}"
|
|
fi
|
|
;;
|
|
branch)
|
|
SCOPE_KEY="ref-${REF_NAME}"
|
|
;;
|
|
repo)
|
|
SCOPE_KEY="repo"
|
|
;;
|
|
*)
|
|
# Fall back to pr-scoped (most conservative) rather than repo-scoped to limit blast radius
|
|
echo "::warning::Unknown memory_scope '${INPUT_MEMORY_SCOPE}', defaulting to 'pr'"
|
|
if [ -n "$EVENT_PR_NUMBER" ]; then
|
|
SCOPE_KEY="pr-${EVENT_PR_NUMBER}"
|
|
else
|
|
SCOPE_KEY="ref-${REF_NAME}"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "key=${PREFIX}-${SCOPE_KEY}" >> "$GITHUB_OUTPUT"
|
|
echo "restore-keys=${PREFIX}-" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Restore agent memory
|
|
if: inputs.enable_memory == 'true'
|
|
id: restore-memory
|
|
uses: actions/cache/restore@v6
|
|
with:
|
|
path: |
|
|
~/.deepagents/${{ inputs.agent_name }}/
|
|
~/.deepagents/.state/sessions.db
|
|
~/.deepagents/.state/sessions.db-wal
|
|
~/.deepagents/.state/sessions.db-shm
|
|
${{ inputs.working_directory }}/.deepagents/AGENTS.md
|
|
key: ${{ steps.cache-key.outputs.key }}-${{ github.run_id }}
|
|
restore-keys: |
|
|
${{ steps.cache-key.outputs.key }}-
|
|
${{ steps.cache-key.outputs.restore-keys }}
|
|
|
|
- name: Install deepagents-code
|
|
shell: bash
|
|
env:
|
|
INPUT_CLI_VERSION: ${{ inputs.cli_version }}
|
|
run: |
|
|
# 0.1.0 is the floor for the always-present flags (--agent,
|
|
# --shell-allow-list, --non-interactive). Optional inputs below may map
|
|
# to flags added in later releases, so pinning an old cli_version while
|
|
# setting a newer optional input can still hit an "unknown flag" error
|
|
# from dcode that this floor check won't catch.
|
|
MIN_CLI_VERSION="0.1.0"
|
|
|
|
if [ -n "$INPUT_CLI_VERSION" ]; then
|
|
if ! printf '%s\n%s' "$MIN_CLI_VERSION" "$INPUT_CLI_VERSION" \
|
|
| sort -V | head -1 | grep -qx "$MIN_CLI_VERSION"; then
|
|
echo "::error::cli_version '${INPUT_CLI_VERSION}' is below the minimum '${MIN_CLI_VERSION}' required by this action"
|
|
exit 1
|
|
fi
|
|
uvx --from "deepagents-code==${INPUT_CLI_VERSION}" dcode --version
|
|
else
|
|
uvx --from deepagents-code dcode --version
|
|
fi
|
|
|
|
- name: Install skills
|
|
if: inputs.skills_repo != ''
|
|
shell: bash
|
|
env:
|
|
INPUT_SKILLS_REPO: ${{ inputs.skills_repo }}
|
|
GITHUB_TOKEN: ${{ inputs.github_token }}
|
|
working-directory: ${{ inputs.working_directory }}
|
|
run: |
|
|
# Build clone URL — check for full URLs first to avoid misinterpreting @ in git@... URLs
|
|
if [[ "$INPUT_SKILLS_REPO" == https://* || "$INPUT_SKILLS_REPO" == git@* ]]; then
|
|
CLONE_URL="$INPUT_SKILLS_REPO"
|
|
REF=""
|
|
elif [[ "$INPUT_SKILLS_REPO" == *"@"* ]]; then
|
|
REPO="${INPUT_SKILLS_REPO%%@*}"
|
|
REF="${INPUT_SKILLS_REPO##*@}"
|
|
CLONE_URL="https://github.com/${REPO}.git"
|
|
else
|
|
CLONE_URL="https://github.com/${INPUT_SKILLS_REPO}.git"
|
|
REF=""
|
|
fi
|
|
|
|
SKILLS_DIR=".deepagents/skills"
|
|
mkdir -p "$SKILLS_DIR"
|
|
|
|
CLONE_DIR=$(mktemp -d)
|
|
trap 'rm -rf "$CLONE_DIR"' EXIT
|
|
|
|
CLONE_ARGS=(gh repo clone "$CLONE_URL" "$CLONE_DIR" --)
|
|
CLONE_ARGS+=(--depth 1)
|
|
if [ -n "$REF" ]; then
|
|
CLONE_ARGS+=(--branch "$REF")
|
|
fi
|
|
|
|
if ! "${CLONE_ARGS[@]}"; then
|
|
echo "::error::Failed to clone skills repository '${INPUT_SKILLS_REPO}'. Verify the repo exists and your github_token has access."
|
|
exit 1
|
|
fi
|
|
|
|
SKILL_COUNT=0
|
|
while IFS= read -r skill_file; do
|
|
skill_dir=$(dirname "$skill_file")
|
|
skill_name=$(basename "$skill_dir")
|
|
cp -r "$skill_dir" "$SKILLS_DIR/$skill_name"
|
|
echo "Installed skill: $skill_name"
|
|
((++SKILL_COUNT))
|
|
done < <(find "$CLONE_DIR" -name "SKILL.md" -type f)
|
|
|
|
if [ "$SKILL_COUNT" -eq 0 ]; then
|
|
echo "::error::No skills found in ${INPUT_SKILLS_REPO} — expected at least one directory containing SKILL.md"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Run dcode
|
|
id: run-agent
|
|
shell: bash
|
|
working-directory: ${{ inputs.working_directory }}
|
|
env:
|
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
|
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
GOOGLE_API_KEY: ${{ inputs.google_api_key }}
|
|
GITHUB_TOKEN: ${{ inputs.github_token }}
|
|
INPUT_MODEL: ${{ inputs.model }}
|
|
INPUT_MODEL_PARAMS: ${{ inputs.model_params }}
|
|
INPUT_MAX_RETRIES: ${{ inputs.max_retries }}
|
|
INPUT_PROFILE_OVERRIDE: ${{ inputs.profile_override }}
|
|
INPUT_PROMPT: ${{ inputs.prompt }}
|
|
INPUT_AGENT_NAME: ${{ inputs.agent_name }}
|
|
INPUT_CLI_VERSION: ${{ inputs.cli_version }}
|
|
INPUT_TIMEOUT: ${{ inputs.timeout }}
|
|
INPUT_SHELL_ALLOW_LIST: ${{ inputs.shell_allow_list }}
|
|
INPUT_SKILL: ${{ inputs.skill }}
|
|
INPUT_STARTUP_CMD: ${{ inputs.startup_cmd }}
|
|
INPUT_SANDBOX: ${{ inputs.sandbox }}
|
|
INPUT_SANDBOX_ID: ${{ inputs.sandbox_id }}
|
|
INPUT_SANDBOX_SNAPSHOT_NAME: ${{ inputs.sandbox_snapshot_name }}
|
|
INPUT_SANDBOX_SETUP: ${{ inputs.sandbox_setup }}
|
|
INPUT_MCP_CONFIG: ${{ inputs.mcp_config }}
|
|
INPUT_NO_MCP: ${{ inputs.no_mcp }}
|
|
INPUT_TRUST_PROJECT_MCP: ${{ inputs.trust_project_mcp }}
|
|
INPUT_INTERPRETER: ${{ inputs.interpreter }}
|
|
INPUT_INTERPRETER_TOOLS: ${{ inputs.interpreter_tools }}
|
|
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
|
|
INPUT_TASK_TIMEOUT: ${{ inputs.task_timeout }}
|
|
INPUT_RUBRIC: ${{ inputs.rubric }}
|
|
INPUT_RUBRIC_MODEL: ${{ inputs.rubric_model }}
|
|
INPUT_RUBRIC_MAX_ITERATIONS: ${{ inputs.rubric_max_iterations }}
|
|
INPUT_QUIET: ${{ inputs.quiet }}
|
|
INPUT_NO_STREAM: ${{ inputs.no_stream }}
|
|
INPUT_JSON: ${{ inputs.json }}
|
|
INPUT_STDIN: ${{ inputs.stdin }}
|
|
run: |
|
|
append_value_flag() {
|
|
local value="$1"
|
|
local flag="$2"
|
|
if [ -n "$value" ]; then
|
|
CMD+=("$flag" "$value")
|
|
fi
|
|
}
|
|
|
|
append_bool_flag() {
|
|
local value="$1"
|
|
local flag="$2"
|
|
case "$value" in
|
|
true)
|
|
CMD+=("$flag")
|
|
;;
|
|
false|"")
|
|
;;
|
|
*)
|
|
echo "::error::Input for ${flag} must be 'true' or 'false', got '${value}'"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
validate_positive_int() {
|
|
local value="$1"
|
|
local input_name="$2"
|
|
# 10#$value forces base-10 so leading-zero values (e.g. 08) don't trip
|
|
# bash's octal parsing in the -eq test.
|
|
if [ -n "$value" ] && { ! [[ "$value" =~ ^[0-9]+$ ]] || [ "$((10#$value))" -eq 0 ]; }; then
|
|
echo "::error::Invalid ${input_name} '${value}' — must be a positive integer"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_non_negative_int() {
|
|
local value="$1"
|
|
local input_name="$2"
|
|
if [ -n "$value" ] && ! [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
echo "::error::Invalid ${input_name} '${value}' — must be a non-negative integer"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_json_object() {
|
|
local value="$1"
|
|
local input_name="$2"
|
|
# Best-effort: only validate when jq is present. dcode still rejects
|
|
# malformed JSON, but this surfaces an error naming the offending input.
|
|
if [ -n "$value" ] && command -v jq >/dev/null 2>&1; then
|
|
if ! printf '%s' "$value" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
echo "::error::Invalid ${input_name} — must be a JSON object"
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
if [ -n "$INPUT_CLI_VERSION" ]; then
|
|
CMD=(uvx --from "deepagents-code==${INPUT_CLI_VERSION}" dcode)
|
|
else
|
|
CMD=(uvx --from deepagents-code dcode)
|
|
fi
|
|
|
|
validate_positive_int "$INPUT_TIMEOUT" "timeout"
|
|
validate_non_negative_int "$INPUT_MAX_RETRIES" "max_retries"
|
|
validate_positive_int "$INPUT_MAX_TURNS" "max_turns"
|
|
validate_positive_int "$INPUT_TASK_TIMEOUT" "task_timeout"
|
|
validate_positive_int "$INPUT_RUBRIC_MAX_ITERATIONS" "rubric_max_iterations"
|
|
validate_json_object "$INPUT_MODEL_PARAMS" "model_params"
|
|
validate_json_object "$INPUT_PROFILE_OVERRIDE" "profile_override"
|
|
|
|
append_value_flag "$INPUT_AGENT_NAME" "--agent"
|
|
append_value_flag "$INPUT_SHELL_ALLOW_LIST" "--shell-allow-list"
|
|
append_value_flag "$INPUT_MODEL" "--model"
|
|
append_value_flag "$INPUT_MODEL_PARAMS" "--model-params"
|
|
append_value_flag "$INPUT_MAX_RETRIES" "--max-retries"
|
|
append_value_flag "$INPUT_PROFILE_OVERRIDE" "--profile-override"
|
|
append_value_flag "$INPUT_SKILL" "--skill"
|
|
append_value_flag "$INPUT_STARTUP_CMD" "--startup-cmd"
|
|
append_value_flag "$INPUT_SANDBOX" "--sandbox"
|
|
append_value_flag "$INPUT_SANDBOX_ID" "--sandbox-id"
|
|
append_value_flag "$INPUT_SANDBOX_SNAPSHOT_NAME" "--sandbox-snapshot-name"
|
|
append_value_flag "$INPUT_SANDBOX_SETUP" "--sandbox-setup"
|
|
append_value_flag "$INPUT_MCP_CONFIG" "--mcp-config"
|
|
append_bool_flag "$INPUT_NO_MCP" "--no-mcp"
|
|
append_bool_flag "$INPUT_TRUST_PROJECT_MCP" "--trust-project-mcp"
|
|
case "$INPUT_INTERPRETER" in
|
|
true)
|
|
CMD+=("--interpreter")
|
|
;;
|
|
false)
|
|
CMD+=("--no-interpreter")
|
|
;;
|
|
"")
|
|
;;
|
|
*)
|
|
echo "::error::Input for interpreter must be 'true', 'false', or empty, got '${INPUT_INTERPRETER}'"
|
|
exit 1
|
|
;;
|
|
esac
|
|
append_value_flag "$INPUT_INTERPRETER_TOOLS" "--interpreter-tools"
|
|
append_value_flag "$INPUT_MAX_TURNS" "--max-turns"
|
|
append_value_flag "$INPUT_TASK_TIMEOUT" "--timeout"
|
|
append_value_flag "$INPUT_RUBRIC" "--rubric"
|
|
append_value_flag "$INPUT_RUBRIC_MODEL" "--rubric-model"
|
|
append_value_flag "$INPUT_RUBRIC_MAX_ITERATIONS" "--rubric-max-iterations"
|
|
append_bool_flag "$INPUT_QUIET" "--quiet"
|
|
append_bool_flag "$INPUT_NO_STREAM" "--no-stream"
|
|
append_bool_flag "$INPUT_JSON" "--json"
|
|
case "$INPUT_STDIN" in
|
|
true|false|"")
|
|
;;
|
|
*)
|
|
echo "::error::Input for stdin must be 'true' or 'false', got '${INPUT_STDIN}'"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# stdin + skill can't run headless: with --skill and no positional prompt,
|
|
# dcode routes piped input to the interactive TUI path. The non-stdin path
|
|
# passes the prompt via --non-interactive, which supports --skill fine.
|
|
if [ "$INPUT_STDIN" = "true" ] && [ -n "$INPUT_SKILL" ]; then
|
|
echo "::error::stdin cannot be combined with skill — use stdin: false so the prompt is passed via --non-interactive"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$INPUT_PROMPT" ]; then
|
|
echo "::error::prompt is empty — provide the task for dcode to run"
|
|
exit 1
|
|
fi
|
|
|
|
OUTPUT_FILE=$(mktemp)
|
|
trap 'rm -f "$OUTPUT_FILE"' EXIT
|
|
|
|
# 10#$INPUT_TIMEOUT forces base-10: validate_positive_int accepts
|
|
# leading-zero values (e.g. 08), which bare arithmetic would misparse as
|
|
# octal and abort on (8 and 9 aren't valid octal digits).
|
|
TIMEOUT_SECS=$((10#$INPUT_TIMEOUT * 60))
|
|
# set +e: let a non-zero agent exit through so we can capture it below
|
|
# instead of aborting the step (composite bash runs with set -e).
|
|
set +e
|
|
if [ "$INPUT_STDIN" = "true" ]; then
|
|
# stdin is special-cased: the prompt is piped in rather than passed
|
|
# positionally. Feed it via process substitution so the agent — not
|
|
# printf — is the pipeline head; a classic `printf | timeout` pipe
|
|
# would surface printf's SIGPIPE (agent closes stdin early on a large
|
|
# prompt) as the captured exit code.
|
|
timeout "${TIMEOUT_SECS}" "${CMD[@]}" "--stdin" < <(printf '%s' "$INPUT_PROMPT") 2>&1 | tee "$OUTPUT_FILE"
|
|
else
|
|
timeout "${TIMEOUT_SECS}" "${CMD[@]}" "--non-interactive" "$INPUT_PROMPT" 2>&1 | tee "$OUTPUT_FILE"
|
|
fi
|
|
# PIPESTATUS[0] is the agent/timeout stage (the pipeline head), so tee's
|
|
# exit can't mask the agent's real code. Read before any other command
|
|
# runs, since PIPESTATUS reflects the most recent pipeline.
|
|
EXIT_CODE=${PIPESTATUS[0]}
|
|
|
|
# The random delimiter stops untrusted agent output from forging extra
|
|
# $GITHUB_OUTPUT entries (a line equal to the delimiter would close the
|
|
# heredoc early). If openssl is unavailable, fall back to /dev/urandom
|
|
# rather than silently degrading to the guessable constant "DEEPAGENTS_".
|
|
RAND_HEX=$(openssl rand -hex 16 2>/dev/null)
|
|
if [ -z "$RAND_HEX" ]; then
|
|
RAND_HEX=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
fi
|
|
if [ -z "$RAND_HEX" ]; then
|
|
echo "::error::could not generate a random output delimiter"
|
|
# Preserve a failing agent code; otherwise fail with 1 since we can't
|
|
# safely emit output.
|
|
[ "$EXIT_CODE" -ne 0 ] && exit "$EXIT_CODE"
|
|
exit 1
|
|
fi
|
|
DELIMITER="DEEPAGENTS_${RAND_HEX}"
|
|
|
|
# Keep `set -e` disabled while writing outputs so a write failure can't
|
|
# mask the agent's exit code — but surface it as a warning so an empty
|
|
# or truncated `response` output isn't mistaken for the agent producing
|
|
# no output.
|
|
if ! {
|
|
echo "exit_code=$EXIT_CODE"
|
|
echo "response<<${DELIMITER}"
|
|
cat "$OUTPUT_FILE"
|
|
echo "${DELIMITER}"
|
|
} >> "$GITHUB_OUTPUT"; then
|
|
echo "::warning::failed to write agent output to \$GITHUB_OUTPUT — the 'response' output may be empty or truncated"
|
|
fi
|
|
|
|
exit "$EXIT_CODE"
|
|
|
|
- name: Save agent memory
|
|
if: inputs.enable_memory == 'true' && steps.cache-key.outputs.key != '' && always()
|
|
uses: actions/cache/save@v6
|
|
with:
|
|
path: |
|
|
~/.deepagents/${{ inputs.agent_name }}/
|
|
~/.deepagents/.state/sessions.db
|
|
~/.deepagents/.state/sessions.db-wal
|
|
~/.deepagents/.state/sessions.db-shm
|
|
${{ inputs.working_directory }}/.deepagents/AGENTS.md
|
|
key: ${{ steps.cache-key.outputs.key }}-${{ github.run_id }}
|