forked from langchain-ai/deepagents
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
539 lines (508 loc) · 20.3 KB
/
Copy pathaction.yml
File metadata and controls
539 lines (508 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
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 }}