strands-coder 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strands_coder-0.1.0/.github/workflows/agent.yml +165 -0
- strands_coder-0.1.0/.github/workflows/auto-release.yml +85 -0
- strands_coder-0.1.0/.github/workflows/control.yml +395 -0
- strands_coder-0.1.0/.gitignore +2 -0
- strands_coder-0.1.0/LICENSE +201 -0
- strands_coder-0.1.0/PKG-INFO +799 -0
- strands_coder-0.1.0/README.md +755 -0
- strands_coder-0.1.0/SYSTEM_PROMPT.md +330 -0
- strands_coder-0.1.0/action.yml +161 -0
- strands_coder-0.1.0/docs/CNAME +1 -0
- strands_coder-0.1.0/docs/index.html +2082 -0
- strands_coder-0.1.0/pyproject.toml +189 -0
- strands_coder-0.1.0/strands_coder/__init__.py +53 -0
- strands_coder-0.1.0/strands_coder/agent_runner.py +443 -0
- strands_coder-0.1.0/strands_coder/context.py +819 -0
- strands_coder-0.1.0/strands_coder/tools/__init__.py +43 -0
- strands_coder-0.1.0/strands_coder/tools/create_subagent.py +665 -0
- strands_coder-0.1.0/strands_coder/tools/github_tools.py +876 -0
- strands_coder-0.1.0/strands_coder/tools/projects.py +1594 -0
- strands_coder-0.1.0/strands_coder/tools/scheduler.py +768 -0
- strands_coder-0.1.0/strands_coder/tools/store_in_kb.py +250 -0
- strands_coder-0.1.0/strands_coder/tools/system_prompt.py +488 -0
- strands_coder-0.1.0/strands_coder/tools/use_github.py +453 -0
- strands_coder-0.1.0/tools/use_langfuse.py +995 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
name: Agent
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Autonomous run every 4 hours - general maintenance and discovery
|
|
6
|
+
# - cron: '0 */4 * * *'
|
|
7
|
+
# Additional scheduled jobs are handled by control.yml (hourly checks)
|
|
8
|
+
# Use the scheduler tool to configure custom schedules via AGENT_SCHEDULES variable
|
|
9
|
+
issues:
|
|
10
|
+
types: [opened, edited, closed, reopened, assigned, unassigned, labeled, unlabeled]
|
|
11
|
+
issue_comment:
|
|
12
|
+
types: [created, edited, deleted]
|
|
13
|
+
pull_request:
|
|
14
|
+
types: [opened, closed, edited, reopened, synchronize, ready_for_review]
|
|
15
|
+
pull_request_review:
|
|
16
|
+
types: [submitted, edited]
|
|
17
|
+
discussion:
|
|
18
|
+
types: [created, edited, answered, unanswered, category_changed, labeled, unlabeled, transferred, pinned, unpinned, locked, unlocked]
|
|
19
|
+
discussion_comment:
|
|
20
|
+
types: [created, edited, deleted]
|
|
21
|
+
pull_request_review_comment:
|
|
22
|
+
types: [created, edited]
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
prompt:
|
|
26
|
+
description: 'Prompt for agent to perform'
|
|
27
|
+
required: false
|
|
28
|
+
type: string
|
|
29
|
+
system_prompt:
|
|
30
|
+
description: 'Additional system prompt instructions'
|
|
31
|
+
required: false
|
|
32
|
+
type: string
|
|
33
|
+
tools:
|
|
34
|
+
description: 'Tool config (e.g., strands_tools:shell;strands_coder:use_github)'
|
|
35
|
+
required: false
|
|
36
|
+
type: string
|
|
37
|
+
model:
|
|
38
|
+
description: 'Model ID'
|
|
39
|
+
default: "global.anthropic.claude-opus-4-5-20251101-v1:0"
|
|
40
|
+
required: false
|
|
41
|
+
type: string
|
|
42
|
+
max_tokens:
|
|
43
|
+
description: 'Max tokens'
|
|
44
|
+
default: "60000"
|
|
45
|
+
required: false
|
|
46
|
+
type: string
|
|
47
|
+
|
|
48
|
+
permissions: write-all
|
|
49
|
+
|
|
50
|
+
jobs:
|
|
51
|
+
agent:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
steps:
|
|
54
|
+
- name: Check user authorization
|
|
55
|
+
id: check-auth
|
|
56
|
+
run: |
|
|
57
|
+
# For scheduled runs, always authorize
|
|
58
|
+
if [ "${{ github.event_name }}" = "schedule" ]; then
|
|
59
|
+
echo "✅ Scheduled run - authorized"
|
|
60
|
+
echo "authorized=true" >> $GITHUB_OUTPUT
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# For workflow_dispatch from control.yml (github-actions[bot])
|
|
65
|
+
if [ "${{ github.actor }}" = "github-actions[bot]" ]; then
|
|
66
|
+
echo "✅ Control loop dispatch - authorized"
|
|
67
|
+
echo "authorized=true" >> $GITHUB_OUTPUT
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# For manual/event-triggered runs, check authorization
|
|
72
|
+
AUTHORIZED_USERS="${{ secrets.AUTHORIZED_USERS }}"
|
|
73
|
+
|
|
74
|
+
echo "Checking authorization for user: ${{ github.actor }}"
|
|
75
|
+
|
|
76
|
+
if [[ ",$AUTHORIZED_USERS," == *",${{ github.actor }},"* ]]; then
|
|
77
|
+
echo "✅ User ${{ github.actor }} is authorized"
|
|
78
|
+
echo "authorized=true" >> $GITHUB_OUTPUT
|
|
79
|
+
else
|
|
80
|
+
echo "❌ User ${{ github.actor }} is NOT authorized"
|
|
81
|
+
echo "Authorized users: $AUTHORIZED_USERS"
|
|
82
|
+
echo "🚫 UNAUTHORIZED ACCESS ATTEMPT"
|
|
83
|
+
echo "Repository: ${{ github.repository }}"
|
|
84
|
+
echo "Event: ${{ github.event_name }}"
|
|
85
|
+
echo "Time: $(date)"
|
|
86
|
+
echo "Contact repository administrators for access."
|
|
87
|
+
echo "authorized=false" >> $GITHUB_OUTPUT
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
- name: Checkout code
|
|
92
|
+
if: steps.check-auth.outputs.authorized == 'true'
|
|
93
|
+
uses: actions/checkout@v4
|
|
94
|
+
with:
|
|
95
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
96
|
+
|
|
97
|
+
- name: Run Strands Agent
|
|
98
|
+
if: steps.check-auth.outputs.authorized == 'true'
|
|
99
|
+
uses: ./
|
|
100
|
+
env:
|
|
101
|
+
# GitHub tokens
|
|
102
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
103
|
+
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
|
104
|
+
|
|
105
|
+
# Model provider API keys (set the one you need based on STRANDS_PROVIDER)
|
|
106
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
107
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
108
|
+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
109
|
+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
|
110
|
+
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
|
111
|
+
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
|
|
112
|
+
WRITER_API_KEY: ${{ secrets.WRITER_API_KEY }}
|
|
113
|
+
LITELLM_API_KEY: ${{ secrets.LITELLM_API_KEY }}
|
|
114
|
+
LLAMAAPI_API_KEY: ${{ secrets.LLAMAAPI_API_KEY }}
|
|
115
|
+
AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
|
|
116
|
+
|
|
117
|
+
# Advanced model configuration
|
|
118
|
+
STRANDS_ADDITIONAL_REQUEST_FIELDS: ${{ vars.STRANDS_ADDITIONAL_REQUEST_FIELDS }}
|
|
119
|
+
|
|
120
|
+
# MCP servers
|
|
121
|
+
MCP_SERVERS: ${{ vars.MCP_SERVERS }}
|
|
122
|
+
|
|
123
|
+
# Project & Knowledge Base
|
|
124
|
+
STRANDS_CODER_PROJECT_ID: ${{ vars.STRANDS_CODER_PROJECT_ID }}
|
|
125
|
+
STRANDS_KNOWLEDGE_BASE_ID: ${{ vars.STRANDS_KNOWLEDGE_BASE_ID }}
|
|
126
|
+
|
|
127
|
+
# Session persistence
|
|
128
|
+
S3_SESSION_BUCKET: ${{ vars.S3_SESSION_BUCKET }}
|
|
129
|
+
S3_SESSION_PREFIX: ${{ vars.S3_SESSION_PREFIX }}
|
|
130
|
+
|
|
131
|
+
# Slack integration
|
|
132
|
+
SLACK_APP_TOKEN: ${{ secrets.SLACK_APP_TOKEN }}
|
|
133
|
+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
134
|
+
|
|
135
|
+
# Observability (Langfuse)
|
|
136
|
+
LANGFUSE_BASE_URL: ${{ secrets.LANGFUSE_BASE_URL }}
|
|
137
|
+
LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
|
|
138
|
+
LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
|
|
139
|
+
|
|
140
|
+
# Advanced settings
|
|
141
|
+
STRANDS_TOOLS_DIRECTORY: ${{ vars.STRANDS_TOOLS_DIRECTORY }}
|
|
142
|
+
with:
|
|
143
|
+
prompt: |
|
|
144
|
+
${{
|
|
145
|
+
github.event.inputs.prompt ||
|
|
146
|
+
(github.event_name == 'schedule' && 'I am running on a scheduled basis (every 4 hours). I will check the repository status, review open issues and PRs, and provide insights or suggestions for improvements, work on active tracked tasks and discover new opportunities to work on. I have full GitHub context available.') ||
|
|
147
|
+
'I received a GitHub event and am running autonomously. I will analyze the context and take appropriate action. I have full GitHub event details available.'
|
|
148
|
+
}}
|
|
149
|
+
|
|
150
|
+
# Model configuration
|
|
151
|
+
provider: ${{ github.event.inputs.provider || vars.STRANDS_PROVIDER || 'bedrock' }}
|
|
152
|
+
model: ${{ github.event.inputs.model || vars.STRANDS_MODEL_ID || 'global.anthropic.claude-opus-4-5-20251101-v1:0' }}
|
|
153
|
+
max_tokens: ${{ vars.STRANDS_MAX_TOKENS || '60000' }}
|
|
154
|
+
|
|
155
|
+
# System prompt configuration
|
|
156
|
+
system_prompt: ${{ github.event.inputs.system_prompt || vars.SYSTEM_PROMPT || vars.INPUT_SYSTEM_PROMPT || 'You are a restricted GitHub agent for this repository, powered by Strands Agents SDK. Only authorized users can trigger your execution.' }}
|
|
157
|
+
|
|
158
|
+
# Tool configuration
|
|
159
|
+
tools: ${{ github.event.inputs.tools || vars.STRANDS_TOOLS || 'strands_tools:shell,retrieve,slack;strands_coder:use_github,create_subagent,system_prompt,store_in_kb,projects,scheduler' }}
|
|
160
|
+
|
|
161
|
+
# AWS configuration
|
|
162
|
+
aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
|
|
163
|
+
aws_region: ${{ secrets.AWS_REGION || 'us-west-2' }}
|
|
164
|
+
git_user_email: "217235299+strands-agent@users.noreply.github.com"
|
|
165
|
+
git_user_name: "strands-agent"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Auto Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
packages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
auto-release:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout code
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
uses: actions/setup-python@v4
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.11"
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade pip
|
|
32
|
+
pip install build twine
|
|
33
|
+
|
|
34
|
+
- name: Extract version from tag
|
|
35
|
+
id: get_version
|
|
36
|
+
run: |
|
|
37
|
+
VERSION=${GITHUB_REF#refs/tags/v}
|
|
38
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
39
|
+
echo "Version: $VERSION"
|
|
40
|
+
|
|
41
|
+
- name: Build package
|
|
42
|
+
run: |
|
|
43
|
+
python -m build
|
|
44
|
+
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
env:
|
|
47
|
+
TWINE_USERNAME: __token__
|
|
48
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
49
|
+
run: |
|
|
50
|
+
twine upload dist/*
|
|
51
|
+
|
|
52
|
+
- name: Create GitHub Release
|
|
53
|
+
uses: actions/create-release@v1
|
|
54
|
+
env:
|
|
55
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
56
|
+
with:
|
|
57
|
+
tag_name: ${{ github.ref }}
|
|
58
|
+
release_name: strands-coder v${{ steps.get_version.outputs.version }}
|
|
59
|
+
body: |
|
|
60
|
+
## 📄 strands-coder v${{ steps.get_version.outputs.version }}
|
|
61
|
+
|
|
62
|
+
### 📦 Installation
|
|
63
|
+
```bash
|
|
64
|
+
pip install strands-coder==${{ steps.get_version.outputs.version }}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 🔄 Upgrade
|
|
68
|
+
```bash
|
|
69
|
+
pip install --upgrade strands-coder
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 📋 Changes
|
|
73
|
+
This release includes the latest changes tagged as v${{ steps.get_version.outputs.version }}.
|
|
74
|
+
|
|
75
|
+
**Full Changelog**: https://github.com/cagataycali/strands-coder/releases
|
|
76
|
+
draft: false
|
|
77
|
+
prerelease: false
|
|
78
|
+
|
|
79
|
+
- name: Summary
|
|
80
|
+
run: |
|
|
81
|
+
echo "## 🎉 Release Summary" >> $GITHUB_STEP_SUMMARY
|
|
82
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
83
|
+
echo "- **Version:** v${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
84
|
+
echo "- **PyPI:** https://pypi.org/project/strands-coder/${{ steps.get_version.outputs.version }}/" >> $GITHUB_STEP_SUMMARY
|
|
85
|
+
echo "- **GitHub Release:** https://github.com/cagataycali/strands-coder/releases/tag/v${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
name: Control Loop
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Run every hour at minute 0
|
|
6
|
+
# - cron: '0 * * * *'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
force_check:
|
|
10
|
+
description: 'Force check schedules regardless of time'
|
|
11
|
+
required: false
|
|
12
|
+
type: boolean
|
|
13
|
+
default: false
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
actions: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
control:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- name: Check Schedules and Dispatch Jobs
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
+
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
|
|
27
|
+
AGENT_SCHEDULES: ${{ vars.AGENT_SCHEDULES }}
|
|
28
|
+
run: |
|
|
29
|
+
#!/bin/bash
|
|
30
|
+
set -e
|
|
31
|
+
|
|
32
|
+
echo "🕐 Control Loop - $(date -u '+%Y-%m-%d %H:%M') UTC"
|
|
33
|
+
echo "Repository: ${{ github.repository }}"
|
|
34
|
+
|
|
35
|
+
# Parse AGENT_SCHEDULES JSON
|
|
36
|
+
if [ -z "$AGENT_SCHEDULES" ] || [ "$AGENT_SCHEDULES" = "{}" ]; then
|
|
37
|
+
echo "ℹ️ No schedules configured (AGENT_SCHEDULES is empty)"
|
|
38
|
+
echo "To add schedules, use the scheduler tool or set the AGENT_SCHEDULES variable"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Get current time components (UTC)
|
|
43
|
+
CURRENT_MINUTE=$(date -u '+%M' | sed 's/^0//')
|
|
44
|
+
CURRENT_HOUR=$(date -u '+%H' | sed 's/^0//')
|
|
45
|
+
CURRENT_DAY=$(date -u '+%d' | sed 's/^0//')
|
|
46
|
+
CURRENT_MONTH=$(date -u '+%m' | sed 's/^0//')
|
|
47
|
+
CURRENT_DOW=$(date -u '+%w') # 0=Sunday
|
|
48
|
+
CURRENT_EPOCH=$(date -u '+%s')
|
|
49
|
+
|
|
50
|
+
# Handle empty values (when minute/hour is 0)
|
|
51
|
+
[ -z "$CURRENT_MINUTE" ] && CURRENT_MINUTE=0
|
|
52
|
+
[ -z "$CURRENT_HOUR" ] && CURRENT_HOUR=0
|
|
53
|
+
[ -z "$CURRENT_DAY" ] && CURRENT_DAY=1
|
|
54
|
+
[ -z "$CURRENT_MONTH" ] && CURRENT_MONTH=1
|
|
55
|
+
|
|
56
|
+
echo "Current time: minute=$CURRENT_MINUTE hour=$CURRENT_HOUR day=$CURRENT_DAY month=$CURRENT_MONTH dow=$CURRENT_DOW epoch=$CURRENT_EPOCH"
|
|
57
|
+
|
|
58
|
+
# Window for catching missed jobs (24 hours = 86400 seconds)
|
|
59
|
+
# This handles GitHub Actions schedule delays/skips
|
|
60
|
+
# Large window ensures daily jobs are caught even if control loop was down
|
|
61
|
+
CATCH_UP_WINDOW=86400
|
|
62
|
+
|
|
63
|
+
# Function to check if a cron field matches
|
|
64
|
+
cron_field_matches() {
|
|
65
|
+
local field="$1"
|
|
66
|
+
local value="$2"
|
|
67
|
+
|
|
68
|
+
# Wildcard matches all
|
|
69
|
+
if [ "$field" = "*" ]; then
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Step values like */15
|
|
74
|
+
if [[ "$field" == "*/"* ]]; then
|
|
75
|
+
local step="${field#*/}"
|
|
76
|
+
if [ $((value % step)) -eq 0 ]; then
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
return 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Lists like 1,3,5
|
|
83
|
+
if [[ "$field" == *","* ]]; then
|
|
84
|
+
IFS=',' read -ra VALUES <<< "$field"
|
|
85
|
+
for v in "${VALUES[@]}"; do
|
|
86
|
+
if [ "$v" -eq "$value" ] 2>/dev/null; then
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Ranges like 1-5
|
|
94
|
+
if [[ "$field" == *"-"* ]] && [[ "$field" != *"/"* ]]; then
|
|
95
|
+
local start="${field%-*}"
|
|
96
|
+
local end="${field#*-}"
|
|
97
|
+
if [ "$value" -ge "$start" ] && [ "$value" -le "$end" ]; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
return 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Direct match
|
|
104
|
+
if [ "$field" -eq "$value" ] 2>/dev/null; then
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
return 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Function to check if a cron should have triggered within a time window
|
|
112
|
+
# This uses a window-based approach instead of exact minute matching
|
|
113
|
+
cron_should_trigger() {
|
|
114
|
+
local cron="$1"
|
|
115
|
+
local last_triggered="$2" # epoch timestamp or empty
|
|
116
|
+
|
|
117
|
+
read -r cron_minute cron_hour cron_dom cron_month cron_dow <<< "$cron"
|
|
118
|
+
|
|
119
|
+
# If no last_triggered, assume it never ran (use epoch 0)
|
|
120
|
+
# This ensures the job will trigger on its first scheduled window
|
|
121
|
+
if [ -z "$last_triggered" ] || [ "$last_triggered" = "null" ] || [ "$last_triggered" = "0" ]; then
|
|
122
|
+
last_triggered=0
|
|
123
|
+
echo " (No previous run recorded)"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Calculate the most recent scheduled time based on cron
|
|
127
|
+
# For simplicity, check if we're within the scheduled hour
|
|
128
|
+
|
|
129
|
+
# Get target hour (handle wildcards)
|
|
130
|
+
local target_hour="$cron_hour"
|
|
131
|
+
if [ "$target_hour" = "*" ]; then
|
|
132
|
+
target_hour="$CURRENT_HOUR"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Get target minute (handle wildcards)
|
|
136
|
+
local target_minute="$cron_minute"
|
|
137
|
+
if [ "$target_minute" = "*" ]; then
|
|
138
|
+
target_minute=0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Check day of week constraint
|
|
142
|
+
if [ "$cron_dow" != "*" ]; then
|
|
143
|
+
if ! cron_field_matches "$cron_dow" "$CURRENT_DOW"; then
|
|
144
|
+
# Not the right day
|
|
145
|
+
return 1
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# Check day of month constraint
|
|
150
|
+
if [ "$cron_dom" != "*" ]; then
|
|
151
|
+
if ! cron_field_matches "$cron_dom" "$CURRENT_DAY"; then
|
|
152
|
+
return 1
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Check month constraint
|
|
157
|
+
if [ "$cron_month" != "*" ]; then
|
|
158
|
+
if ! cron_field_matches "$cron_month" "$CURRENT_MONTH"; then
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Calculate the scheduled epoch for today at target_hour:target_minute
|
|
164
|
+
local today_date=$(date -u '+%Y-%m-%d')
|
|
165
|
+
local scheduled_time="${today_date}T$(printf '%02d' $target_hour):$(printf '%02d' $target_minute):00"
|
|
166
|
+
local scheduled_epoch=$(date -u -d "$scheduled_time" '+%s' 2>/dev/null || echo "0")
|
|
167
|
+
|
|
168
|
+
if [ "$scheduled_epoch" = "0" ]; then
|
|
169
|
+
echo " ⚠️ Failed to calculate scheduled epoch"
|
|
170
|
+
return 1
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
echo " Scheduled time: $scheduled_time (epoch: $scheduled_epoch)"
|
|
174
|
+
echo " Last triggered: $last_triggered"
|
|
175
|
+
echo " Current epoch: $CURRENT_EPOCH"
|
|
176
|
+
|
|
177
|
+
# Check if:
|
|
178
|
+
# 1. The scheduled time has passed (scheduled_epoch <= CURRENT_EPOCH)
|
|
179
|
+
# 2. We haven't triggered since the scheduled time (last_triggered < scheduled_epoch)
|
|
180
|
+
# 3. We're within the catch-up window (CURRENT_EPOCH - scheduled_epoch <= CATCH_UP_WINDOW)
|
|
181
|
+
|
|
182
|
+
if [ "$scheduled_epoch" -le "$CURRENT_EPOCH" ] && \
|
|
183
|
+
[ "$last_triggered" -lt "$scheduled_epoch" ] && \
|
|
184
|
+
[ $((CURRENT_EPOCH - scheduled_epoch)) -le "$CATCH_UP_WINDOW" ]; then
|
|
185
|
+
echo " ✅ Should trigger (scheduled time passed and not yet triggered)"
|
|
186
|
+
return 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
echo " ⏰ Not due (already triggered or not in window)"
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Function to check if run_at time has passed (within window)
|
|
194
|
+
run_at_matches() {
|
|
195
|
+
local run_at="$1"
|
|
196
|
+
|
|
197
|
+
# Parse ISO datetime and convert to epoch
|
|
198
|
+
local run_at_clean=$(echo "$run_at" | sed 's/Z$//')
|
|
199
|
+
local run_at_epoch=$(date -u -d "$run_at_clean" '+%s' 2>/dev/null || echo "0")
|
|
200
|
+
|
|
201
|
+
if [ "$run_at_epoch" = "0" ]; then
|
|
202
|
+
echo " ⚠️ Failed to parse run_at: $run_at"
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# Check if run_at is within the window (-1 to +CATCH_UP_WINDOW/60 minutes from now)
|
|
207
|
+
local diff=$((CURRENT_EPOCH - run_at_epoch))
|
|
208
|
+
local diff_minutes=$((diff / 60))
|
|
209
|
+
local window_minutes=$((CATCH_UP_WINDOW / 60))
|
|
210
|
+
|
|
211
|
+
if [ "$diff_minutes" -ge -1 ] && [ "$diff_minutes" -le "$window_minutes" ]; then
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
return 1
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Parse and check each job
|
|
219
|
+
JOBS_TO_RUN=""
|
|
220
|
+
JOBS_TO_REMOVE=""
|
|
221
|
+
JOBS_TO_UPDATE=""
|
|
222
|
+
JOB_COUNT=0
|
|
223
|
+
|
|
224
|
+
# Use jq to iterate through jobs
|
|
225
|
+
for job_id in $(echo "$AGENT_SCHEDULES" | jq -r '.jobs | keys[]' 2>/dev/null); do
|
|
226
|
+
echo ""
|
|
227
|
+
echo "━━━ Checking job: $job_id ━━━"
|
|
228
|
+
|
|
229
|
+
# Get job details
|
|
230
|
+
enabled=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].enabled // true")
|
|
231
|
+
cron_expr=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].cron // \"\"")
|
|
232
|
+
run_at=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].run_at // \"\"")
|
|
233
|
+
once=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].once // false")
|
|
234
|
+
last_triggered=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].last_triggered // 0")
|
|
235
|
+
|
|
236
|
+
if [ "$enabled" != "true" ]; then
|
|
237
|
+
echo " ⏭️ Skipped (disabled)"
|
|
238
|
+
continue
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
should_run=false
|
|
242
|
+
|
|
243
|
+
# Check cron expression with window-based approach
|
|
244
|
+
if [ -n "$cron_expr" ] && [ "$cron_expr" != "null" ]; then
|
|
245
|
+
echo " Cron: $cron_expr"
|
|
246
|
+
if [ "${{ inputs.force_check }}" = "true" ]; then
|
|
247
|
+
echo " ✅ Force check enabled"
|
|
248
|
+
should_run=true
|
|
249
|
+
elif cron_should_trigger "$cron_expr" "$last_triggered"; then
|
|
250
|
+
should_run=true
|
|
251
|
+
# Mark for timestamp update
|
|
252
|
+
JOBS_TO_UPDATE="$JOBS_TO_UPDATE $job_id"
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Check run_at datetime
|
|
257
|
+
if [ -n "$run_at" ] && [ "$run_at" != "null" ]; then
|
|
258
|
+
echo " Run At: $run_at"
|
|
259
|
+
if [ "${{ inputs.force_check }}" = "true" ] || run_at_matches "$run_at"; then
|
|
260
|
+
echo " ✅ Run At MATCH"
|
|
261
|
+
should_run=true
|
|
262
|
+
if [ "$once" = "true" ]; then
|
|
263
|
+
echo " 🗑️ Will be removed after dispatch (once=true)"
|
|
264
|
+
JOBS_TO_REMOVE="$JOBS_TO_REMOVE $job_id"
|
|
265
|
+
fi
|
|
266
|
+
else
|
|
267
|
+
echo " ⏰ Run At not due yet"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
if [ "$should_run" = "true" ]; then
|
|
272
|
+
JOBS_TO_RUN="$JOBS_TO_RUN $job_id"
|
|
273
|
+
JOB_COUNT=$((JOB_COUNT + 1))
|
|
274
|
+
fi
|
|
275
|
+
done
|
|
276
|
+
|
|
277
|
+
echo ""
|
|
278
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
279
|
+
|
|
280
|
+
if [ $JOB_COUNT -eq 0 ]; then
|
|
281
|
+
echo "📭 No jobs scheduled to run at this time"
|
|
282
|
+
exit 0
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
echo "🚀 Dispatching $JOB_COUNT job(s)..."
|
|
286
|
+
|
|
287
|
+
# Use PAT_TOKEN if available (required for workflow dispatch), fallback to GITHUB_TOKEN
|
|
288
|
+
TOKEN="${PAT_TOKEN:-$GITHUB_TOKEN}"
|
|
289
|
+
|
|
290
|
+
# Track successfully dispatched jobs for timestamp update
|
|
291
|
+
DISPATCHED_JOBS=""
|
|
292
|
+
|
|
293
|
+
# Dispatch each matching job
|
|
294
|
+
for job_id in $JOBS_TO_RUN; do
|
|
295
|
+
echo ""
|
|
296
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
297
|
+
echo "📤 Dispatching: $job_id"
|
|
298
|
+
|
|
299
|
+
# Extract job configuration
|
|
300
|
+
prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].prompt // \"\"")
|
|
301
|
+
system_prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].system_prompt // \"\"")
|
|
302
|
+
tools=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].tools // \"\"")
|
|
303
|
+
model=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].model // \"\"")
|
|
304
|
+
max_tokens=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].max_tokens // \"\"")
|
|
305
|
+
context=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].context // \"\"")
|
|
306
|
+
|
|
307
|
+
echo " Prompt: ${prompt:0:80}..."
|
|
308
|
+
|
|
309
|
+
# Build the full prompt with context
|
|
310
|
+
full_prompt="[Scheduled Job: $job_id]\n\n$prompt"
|
|
311
|
+
if [ -n "$context" ] && [ "$context" != "null" ]; then
|
|
312
|
+
full_prompt="$full_prompt\n\nContext:\n$context"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Build inputs JSON
|
|
316
|
+
inputs_json=$(jq -n \
|
|
317
|
+
--arg prompt "$full_prompt" \
|
|
318
|
+
--arg system_prompt "$system_prompt" \
|
|
319
|
+
--arg tools "$tools" \
|
|
320
|
+
--arg model "$model" \
|
|
321
|
+
--arg max_tokens "$max_tokens" \
|
|
322
|
+
'{prompt: $prompt}
|
|
323
|
+
| if $system_prompt != "" and $system_prompt != "null" then . + {system_prompt: $system_prompt} else . end
|
|
324
|
+
| if $tools != "" and $tools != "null" then . + {tools: $tools} else . end
|
|
325
|
+
| if $model != "" and $model != "null" then . + {model: $model} else . end
|
|
326
|
+
| if $max_tokens != "" and $max_tokens != "null" then . + {max_tokens: $max_tokens} else . end')
|
|
327
|
+
|
|
328
|
+
echo " Inputs: $(echo "$inputs_json" | jq -c .)"
|
|
329
|
+
|
|
330
|
+
# Dispatch the workflow
|
|
331
|
+
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
332
|
+
-H "Accept: application/vnd.github+json" \
|
|
333
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
334
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
335
|
+
"https://api.github.com/repos/${{ github.repository }}/actions/workflows/agent.yml/dispatches" \
|
|
336
|
+
-d "{\"ref\": \"main\", \"inputs\": $inputs_json}")
|
|
337
|
+
|
|
338
|
+
http_code=$(echo "$response" | tail -n1)
|
|
339
|
+
body=$(echo "$response" | sed '$d')
|
|
340
|
+
|
|
341
|
+
if [ "$http_code" = "204" ]; then
|
|
342
|
+
echo " ✅ Dispatched successfully"
|
|
343
|
+
DISPATCHED_JOBS="$DISPATCHED_JOBS $job_id"
|
|
344
|
+
else
|
|
345
|
+
echo " ❌ Failed to dispatch: HTTP $http_code"
|
|
346
|
+
echo " Response: $body"
|
|
347
|
+
fi
|
|
348
|
+
done
|
|
349
|
+
|
|
350
|
+
# Update AGENT_SCHEDULES with last_triggered timestamps and remove once=true jobs
|
|
351
|
+
updated_schedules="$AGENT_SCHEDULES"
|
|
352
|
+
|
|
353
|
+
# Update last_triggered for dispatched cron jobs
|
|
354
|
+
for job_id in $DISPATCHED_JOBS; do
|
|
355
|
+
# Only update if it's a cron job (not a one-time job being removed)
|
|
356
|
+
if echo "$JOBS_TO_UPDATE" | grep -qw "$job_id"; then
|
|
357
|
+
echo ""
|
|
358
|
+
echo "📝 Updating last_triggered for: $job_id"
|
|
359
|
+
updated_schedules=$(echo "$updated_schedules" | jq ".jobs[\"$job_id\"].last_triggered = $CURRENT_EPOCH")
|
|
360
|
+
fi
|
|
361
|
+
done
|
|
362
|
+
|
|
363
|
+
# Remove once=true jobs that have been dispatched
|
|
364
|
+
for job_id in $JOBS_TO_REMOVE; do
|
|
365
|
+
if echo "$DISPATCHED_JOBS" | grep -qw "$job_id"; then
|
|
366
|
+
echo ""
|
|
367
|
+
echo "🗑️ Removing one-time job: $job_id"
|
|
368
|
+
updated_schedules=$(echo "$updated_schedules" | jq "del(.jobs[\"$job_id\"])")
|
|
369
|
+
fi
|
|
370
|
+
done
|
|
371
|
+
|
|
372
|
+
# Save updated schedules if any changes
|
|
373
|
+
if [ "$updated_schedules" != "$AGENT_SCHEDULES" ]; then
|
|
374
|
+
echo ""
|
|
375
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
376
|
+
echo "💾 Saving updated schedule..."
|
|
377
|
+
|
|
378
|
+
update_response=$(curl -s -w "\n%{http_code}" -X PATCH \
|
|
379
|
+
-H "Accept: application/vnd.github+json" \
|
|
380
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
381
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
382
|
+
"https://api.github.com/repos/${{ github.repository }}/actions/variables/AGENT_SCHEDULES" \
|
|
383
|
+
-d "{\"name\": \"AGENT_SCHEDULES\", \"value\": $(echo "$updated_schedules" | jq -c . | jq -Rs .)}")
|
|
384
|
+
|
|
385
|
+
update_code=$(echo "$update_response" | tail -n1)
|
|
386
|
+
if [ "$update_code" = "204" ]; then
|
|
387
|
+
echo " ✅ Schedule updated successfully"
|
|
388
|
+
else
|
|
389
|
+
echo " ⚠️ Failed to update schedule: HTTP $update_code"
|
|
390
|
+
fi
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
echo ""
|
|
394
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
395
|
+
echo "🏁 Control loop complete - dispatched $JOB_COUNT job(s)"
|