Skip to content

Commit 2cc5621

Browse files
committed
feat: add workflow parameter for multi-runner dispatch
The scheduler tool now supports a "workflow" parameter that specifies which GitHub Actions workflow file the control loop should dispatch to. Supported targets: - agent.yml (default) - ubuntu-latest cloud runner - thor.yml - self-hosted Thor (Jetson AGX Thor, GPU, CUDA) - isaac-sim.yml - self-hosted Isaac Sim EC2 (L40S GPU) - Any custom workflow with workflow_dispatch + prompt input Changes: - Added workflow param to scheduler() function signature - Stored in AGENT_SCHEDULES JSON per job - Displayed in list/get/check outputs with runner icons - Backward compatible: defaults to agent.yml when not set - Updated module docstring with workflow examples
1 parent 9695720 commit 2cc5621

1 file changed

Lines changed: 78 additions & 56 deletions

File tree

strands_coder/tools/scheduler.py

Lines changed: 78 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@
2424
"model": "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
2525
"max_tokens": 10000
2626
},
27+
"g1_training": {
28+
"cron": "0 2 * * *",
29+
"enabled": true,
30+
"prompt": "Run G1 humanoid RL training",
31+
"workflow": "thor.yml"
32+
},
33+
"sim_validation": {
34+
"cron": "0 6 * * 1",
35+
"enabled": true,
36+
"prompt": "Validate robot models in Isaac Sim",
37+
"workflow": "isaac-sim.yml"
38+
},
2739
"deploy_friday": {
2840
"run_at": "2024-01-19T15:00:00Z",
2941
"enabled": true,
@@ -126,9 +138,7 @@ def _get_github_variable(repository: str, name: str, token: str) -> dict[str, An
126138
return {"success": False, "message": str(e)}
127139

128140

129-
def _set_github_variable(
130-
repository: str, name: str, value: str, token: str
131-
) -> dict[str, Any]:
141+
def _set_github_variable(repository: str, name: str, value: str, token: str) -> dict[str, Any]:
132142
"""Create or update a GitHub repository variable."""
133143
headers = {
134144
"Accept": "application/vnd.github+json",
@@ -138,19 +148,15 @@ def _set_github_variable(
138148

139149
# Try to update first
140150
url = f"{GITHUB_API_URL}/repos/{repository}/actions/variables/{name}"
141-
response = requests.patch(
142-
url, headers=headers, json={"name": name, "value": value}, timeout=30
143-
)
151+
response = requests.patch(url, headers=headers, json={"name": name, "value": value}, timeout=30)
144152

145153
if response.status_code == 204:
146154
return {"success": True, "message": f"Variable {name} updated"}
147155

148156
# If not found, create it
149157
if response.status_code == 404:
150158
url = f"{GITHUB_API_URL}/repos/{repository}/actions/variables"
151-
response = requests.post(
152-
url, headers=headers, json={"name": name, "value": value}, timeout=30
153-
)
159+
response = requests.post(url, headers=headers, json={"name": name, "value": value}, timeout=30)
154160
if response.status_code == 201:
155161
return {"success": True, "message": f"Variable {name} created"}
156162

@@ -280,22 +286,14 @@ def _get_schedules(repository: str, token: str) -> dict[str, Any]:
280286
return {"jobs": {}, "timezone": "UTC"}
281287

282288
try:
283-
return (
284-
json.loads(result["value"])
285-
if result["value"]
286-
else {"jobs": {}, "timezone": "UTC"}
287-
)
289+
return json.loads(result["value"]) if result["value"] else {"jobs": {}, "timezone": "UTC"}
288290
except json.JSONDecodeError:
289291
return {"jobs": {}, "timezone": "UTC"}
290292

291293

292-
def _save_schedules(
293-
repository: str, schedules: dict[str, Any], token: str
294-
) -> dict[str, Any]:
294+
def _save_schedules(repository: str, schedules: dict[str, Any], token: str) -> dict[str, Any]:
295295
"""Save schedules to GitHub variable."""
296-
return _set_github_variable(
297-
repository, SCHEDULES_VARIABLE, json.dumps(schedules, indent=2), token
298-
)
296+
return _set_github_variable(repository, SCHEDULES_VARIABLE, json.dumps(schedules, indent=2), token)
299297

300298

301299
@tool
@@ -312,12 +310,14 @@ def scheduler(
312310
max_tokens: int | None = None,
313311
context: str | None = None,
314312
repository: str | None = None,
313+
workflow: str | None = None,
315314
) -> dict[str, Any]:
316315
"""Manage scheduled jobs for the autonomous agent.
317316
318317
This tool manages a schedule of jobs stored in GitHub Action variables.
319318
Jobs can be recurring (cron) or one-time (run_at).
320-
The control loop workflow checks this schedule hourly and dispatches jobs.
319+
The control loop workflow checks this schedule hourly and dispatches jobs
320+
to the appropriate workflow file based on the "workflow" field.
321321
322322
Actions:
323323
- "list": List all scheduled jobs
@@ -341,6 +341,10 @@ def scheduler(
341341
max_tokens: Max tokens for model response
342342
context: Additional context to include in the prompt
343343
repository: GitHub repository (defaults to GITHUB_REPOSITORY env var)
344+
workflow: Target workflow file to dispatch to (default: "agent.yml").
345+
Use "thor.yml" for Thor GPU/hardware tasks, "isaac-sim.yml" for
346+
Isaac Sim simulation tasks, or any other workflow filename that
347+
supports workflow_dispatch with prompt/model/tools inputs.
344348
345349
Returns:
346350
Dict with status and operation results
@@ -350,7 +354,7 @@ def scheduler(
350354
# List all jobs
351355
scheduler(action="list")
352356
353-
# Add a recurring daily code review job at 9 AM UTC
357+
# Add a recurring daily code review job at 9 AM UTC (runs on agent.yml / ubuntu)
354358
scheduler(
355359
action="add",
356360
job_id="daily_review",
@@ -360,6 +364,25 @@ def scheduler(
360364
tools="strands_tools:shell;strands_coder:use_github"
361365
)
362366
367+
# Schedule a GPU task on Thor (self-hosted runner)
368+
scheduler(
369+
action="add",
370+
job_id="g1_training",
371+
cron="0 2 * * *",
372+
prompt="Run G1 humanoid RL training with Newton backend",
373+
workflow="thor.yml",
374+
tools="strands_tools:shell,file_read,file_write"
375+
)
376+
377+
# Schedule an Isaac Sim task on EC2 (self-hosted runner)
378+
scheduler(
379+
action="add",
380+
job_id="sim_validation",
381+
cron="0 6 * * 1",
382+
prompt="Validate all robot models in Isaac Sim",
383+
workflow="isaac-sim.yml"
384+
)
385+
363386
# Schedule a one-time deployment for a specific time
364387
scheduler(
365388
action="add",
@@ -369,14 +392,6 @@ def scheduler(
369392
once=True # Auto-remove after execution
370393
)
371394
372-
# Schedule a reminder (one-time, keeps in history)
373-
scheduler(
374-
action="add",
375-
job_id="team_meeting_reminder",
376-
run_at="2024-01-19T09:00:00Z",
377-
prompt="Remind the team about the standup meeting"
378-
)
379-
380395
# Check which jobs should run now (used by control loop)
381396
scheduler(action="check")
382397
@@ -409,28 +424,26 @@ def scheduler(
409424
Examples:
410425
- "2024-01-20T14:00:00Z" = January 20, 2024 at 2:00 PM UTC
411426
- "2024-01-20T09:30:00" = January 20, 2024 at 9:30 AM (assumes UTC)
427+
428+
Workflow Targets:
429+
- "agent.yml" (default) - Runs on ubuntu-latest (cloud agent)
430+
- "thor.yml" - Runs on self-hosted Thor (NVIDIA Jetson AGX Thor, GPU, CUDA)
431+
- "isaac-sim.yml" - Runs on self-hosted Isaac Sim EC2 (L40S GPU, Isaac Sim 5.1)
432+
- Any other workflow file that accepts workflow_dispatch with prompt input
412433
"""
413434
try:
414435
repo = repository or _get_repository()
415436
if not repo:
416437
return {
417438
"status": "error",
418-
"content": [
419-
{
420-
"text": "Error: Repository not specified and GITHUB_REPOSITORY not set"
421-
}
422-
],
439+
"content": [{"text": "Error: Repository not specified and GITHUB_REPOSITORY not set"}],
423440
}
424441

425442
token = _get_github_token()
426443
if not token:
427444
return {
428445
"status": "error",
429-
"content": [
430-
{
431-
"text": "Error: GitHub token not available (PAT_TOKEN or GITHUB_TOKEN)"
432-
}
433-
],
446+
"content": [{"text": "Error: GitHub token not available (PAT_TOKEN or GITHUB_TOKEN)"}],
434447
}
435448

436449
# LIST - Show all jobs
@@ -451,7 +464,15 @@ def scheduler(
451464
for jid, job in jobs.items():
452465
enabled = "✅" if job.get("enabled", True) else "❌"
453466
job_type = "🔄" if job.get("cron") else "📅"
454-
lines.append(f"### {enabled} {job_type} `{jid}`")
467+
target_wf = job.get("workflow", "agent.yml")
468+
runner_icons = {
469+
"agent.yml": "🖥️",
470+
"thor.yml": "🤖",
471+
"isaac-sim.yml": "🎮",
472+
}
473+
runner_icon = runner_icons.get(target_wf, "⚙️")
474+
lines.append(f"### {enabled} {job_type} {runner_icon} `{jid}`")
475+
lines.append(f"- **Workflow:** `{target_wf}`")
455476

456477
if job.get("cron"):
457478
lines.append(f"- **Cron:** `{job['cron']}` (recurring)")
@@ -509,6 +530,7 @@ def scheduler(
509530
"max_tokens": job.get("max_tokens"),
510531
"context": job.get("context"),
511532
"once": job.get("once", False),
533+
"workflow": job.get("workflow", "agent.yml"),
512534
}
513535
)
514536

@@ -521,11 +543,7 @@ def scheduler(
521543
if not jobs_to_run:
522544
return {
523545
"status": "success",
524-
"content": [
525-
{
526-
"text": f"No jobs scheduled to run at {now.strftime('%Y-%m-%d %H:%M')} UTC"
527-
}
528-
],
546+
"content": [{"text": f"No jobs scheduled to run at {now.strftime('%Y-%m-%d %H:%M')} UTC"}],
529547
"jobs_to_run": [],
530548
}
531549

@@ -541,9 +559,7 @@ def scheduler(
541559
lines.append("")
542560

543561
if jobs_to_remove:
544-
lines.append(
545-
f"\n*Removed {len(jobs_to_remove)} one-time job(s): {', '.join(jobs_to_remove)}*"
546-
)
562+
lines.append(f"\n*Removed {len(jobs_to_remove)} one-time job(s): {', '.join(jobs_to_remove)}*")
547563

548564
return {
549565
"status": "success",
@@ -589,9 +605,7 @@ def scheduler(
589605
return {
590606
"status": "error",
591607
"content": [
592-
{
593-
"text": f"Error: Invalid run_at format. Use ISO 8601 (e.g., 2024-01-20T14:00:00Z)"
594-
}
608+
{"text": "Error: Invalid run_at format. Use ISO 8601 (e.g., 2024-01-20T14:00:00Z)"}
595609
],
596610
}
597611

@@ -624,6 +638,8 @@ def scheduler(
624638
schedules["jobs"][job_id]["max_tokens"] = max_tokens
625639
if context:
626640
schedules["jobs"][job_id]["context"] = context
641+
if workflow:
642+
schedules["jobs"][job_id]["workflow"] = workflow
627643

628644
result = _save_schedules(repo, schedules, token)
629645
if not result["success"]:
@@ -635,12 +651,18 @@ def scheduler(
635651
action_word = "updated" if is_update else "added"
636652
schedule_info = f"**Cron:** `{cron}`" if cron else f"**Run At:** `{run_at}`"
637653
once_info = " (once, auto-remove)" if once else ""
654+
workflow_info = f"\n**Workflow:** `{workflow}`" if workflow else ""
655+
prompt_preview = prompt[:100]
638656

639657
return {
640658
"status": "success",
641659
"content": [
642660
{
643-
"text": f"✅ Job `{job_id}` {action_word} successfully\n\n{schedule_info}{once_info}\n**Prompt:** {prompt[:100]}..."
661+
"text": (
662+
f"✅ Job `{job_id}` {action_word} successfully\n\n"
663+
f"{schedule_info}{once_info}{workflow_info}\n"
664+
f"**Prompt:** {prompt_preview}..."
665+
)
644666
}
645667
],
646668
}
@@ -727,8 +749,10 @@ def scheduler(
727749

728750
job = jobs[job_id]
729751
job_type = "🔄 Recurring" if job.get("cron") else "📅 One-time"
752+
target_wf = job.get("workflow", "agent.yml")
730753
lines = [f"## Job: `{job_id}` ({job_type})\n"]
731754
lines.append(f"**Enabled:** {'✅' if job.get('enabled', True) else '❌'}")
755+
lines.append(f"**Workflow:** `{target_wf}`")
732756

733757
if job.get("cron"):
734758
lines.append(f"**Cron:** `{job['cron']}`")
@@ -758,9 +782,7 @@ def scheduler(
758782
return {
759783
"status": "error",
760784
"content": [
761-
{
762-
"text": f"Unknown action: {action}. Valid: list, check, add, remove, enable, disable, get"
763-
}
785+
{"text": f"Unknown action: {action}. Valid: list, check, add, remove, enable, disable, get"}
764786
],
765787
}
766788

0 commit comments

Comments
 (0)