secondshift 0.1.0b12__py3-none-any.whl
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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""DBOS schedules: one per enabled job with a cron schedule."""
|
|
2
|
+
|
|
3
|
+
from dbos import DBOSClient
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
|
|
6
|
+
from secondshift.config.constants import SCHEDULED_JOB_WORKFLOW
|
|
7
|
+
from secondshift.jobs.dao import schedulable_jobs
|
|
8
|
+
from secondshift.runs.orchestration.dbos_runtime import new_dbos_client
|
|
9
|
+
|
|
10
|
+
SCHEDULE_PREFIX = "secondshift-job-"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def schedule_name(job_id: int) -> str:
|
|
14
|
+
return f"{SCHEDULE_PREFIX}{job_id}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def desired_schedules() -> list[dict[str, object]]:
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
"schedule_name": schedule_name(job.pk),
|
|
21
|
+
"workflow_name": SCHEDULED_JOB_WORKFLOW,
|
|
22
|
+
"workflow_class_name": None,
|
|
23
|
+
"schedule": job.cron,
|
|
24
|
+
"context": {"job_id": job.pk},
|
|
25
|
+
"automatic_backfill": False,
|
|
26
|
+
"cron_timezone": job.timezone,
|
|
27
|
+
"queue_name": settings.DBOS_QUEUE_NAME,
|
|
28
|
+
"application_name": settings.DBOS_APPLICATION_NAME,
|
|
29
|
+
}
|
|
30
|
+
for job in schedulable_jobs()
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def synchronize_schedules(client: DBOSClient | None = None) -> int:
|
|
35
|
+
schedules = desired_schedules()
|
|
36
|
+
desired_names = {str(item["schedule_name"]) for item in schedules}
|
|
37
|
+
owns_client = client is None
|
|
38
|
+
client = client or new_dbos_client()
|
|
39
|
+
try:
|
|
40
|
+
client.apply_schedules(schedules) # type: ignore[arg-type]
|
|
41
|
+
existing = client.list_schedules(
|
|
42
|
+
schedule_name_prefix=SCHEDULE_PREFIX,
|
|
43
|
+
application_name=settings.DBOS_APPLICATION_NAME,
|
|
44
|
+
)
|
|
45
|
+
for schedule in existing:
|
|
46
|
+
if schedule["schedule_name"] not in desired_names:
|
|
47
|
+
client.delete_schedule(schedule["schedule_name"])
|
|
48
|
+
finally:
|
|
49
|
+
if owns_client:
|
|
50
|
+
client.destroy()
|
|
51
|
+
return len(schedules)
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tempfile
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dbos import DBOS
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
|
|
11
|
+
from secondshift.config.constants import (
|
|
12
|
+
EXECUTE_RUN_WORKFLOW,
|
|
13
|
+
RETRY_ISSUES_WORKFLOW,
|
|
14
|
+
RETRY_NOTIFICATION_WORKFLOW,
|
|
15
|
+
RETRY_PULL_REQUEST_WORKFLOW,
|
|
16
|
+
SANDBOX_SKILL_DIR,
|
|
17
|
+
SCHEDULED_JOB_WORKFLOW,
|
|
18
|
+
RunSandbox,
|
|
19
|
+
)
|
|
20
|
+
from secondshift.connections.dao import connection_binding, get_connection
|
|
21
|
+
from secondshift.connections.webhooks import deliver_webhook
|
|
22
|
+
from secondshift.jobs.dao import scheduled_job
|
|
23
|
+
from secondshift.jobs.readiness import execution_problems
|
|
24
|
+
from secondshift.jobs.scheduling import next_occurrence
|
|
25
|
+
from secondshift.repositories.dao import lock_repository
|
|
26
|
+
from secondshift.repositories.external_skills import skill_snapshot
|
|
27
|
+
from secondshift.repositories.github_access import github_access
|
|
28
|
+
from secondshift.repositories.models import Repository
|
|
29
|
+
from secondshift.repositories.sources import source_snapshot
|
|
30
|
+
from secondshift.runner.client import get_runner_client
|
|
31
|
+
from secondshift.runner.protocol import RunnerSessionSpec
|
|
32
|
+
from secondshift.runs.artifacts import read_output_excerpt, store_collection
|
|
33
|
+
from secondshift.runs.dao import (
|
|
34
|
+
add_event,
|
|
35
|
+
fail_notification_delivery,
|
|
36
|
+
finish_delivery,
|
|
37
|
+
get_run,
|
|
38
|
+
notification_delivery,
|
|
39
|
+
update_run,
|
|
40
|
+
)
|
|
41
|
+
from secondshift.runs.execution.budgets import Budget, budget_reached_message
|
|
42
|
+
from secondshift.runs.execution.deadlines import run_deadline, timeout_failure
|
|
43
|
+
from secondshift.runs.execution.result import AgentResult
|
|
44
|
+
from secondshift.runs.execution.run import execute_agent_run
|
|
45
|
+
from secondshift.runs.history import pack_earlier_files
|
|
46
|
+
from secondshift.runs.issue_delivery import open_run_issues
|
|
47
|
+
from secondshift.runs.models import Run, RunDelivery, RunFailure
|
|
48
|
+
from secondshift.runs.orchestration.admission import task_admission_problem
|
|
49
|
+
from secondshift.runs.orchestration.reconciliation import (
|
|
50
|
+
WorkflowStatus,
|
|
51
|
+
mark_run_terminal,
|
|
52
|
+
)
|
|
53
|
+
from secondshift.runs.publishing import publish_run
|
|
54
|
+
from secondshift.runs.reporting.collection import collection_summary
|
|
55
|
+
from secondshift.runs.reporting.failures import (
|
|
56
|
+
StageFailure,
|
|
57
|
+
failure_metadata,
|
|
58
|
+
stage_failure,
|
|
59
|
+
)
|
|
60
|
+
from secondshift.runs.reporting.notifications import (
|
|
61
|
+
notification_targets,
|
|
62
|
+
run_report_payload,
|
|
63
|
+
)
|
|
64
|
+
from secondshift.runs.services import create_scheduled_run
|
|
65
|
+
from secondshift.runs.transitions import finish_run, mark_running
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@DBOS.step(name="secondshift.create_scheduled_run")
|
|
71
|
+
def create_scheduled_run_step(scheduled_time: datetime, context: Any) -> int | None:
|
|
72
|
+
job_id = int(context["job_id"])
|
|
73
|
+
job = scheduled_job(job_id)
|
|
74
|
+
if job is None:
|
|
75
|
+
logger.info("skipping disabled or deleted scheduled job %s", job_id)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
run = create_scheduled_run(job, scheduled_time)
|
|
79
|
+
job.next_run_at = next_occurrence(job, after=scheduled_time)
|
|
80
|
+
job.save(update_fields=["next_run_at", "updated_at"])
|
|
81
|
+
if run is None:
|
|
82
|
+
logger.info("skipped scheduled job %s: a run has not finished", job_id)
|
|
83
|
+
return None
|
|
84
|
+
return run.pk
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RunNotReadyError(RuntimeError):
|
|
88
|
+
"""The run's access plan was not ready when it was created.
|
|
89
|
+
|
|
90
|
+
A RuntimeError, so its message, which SecondShift writes, shows in the summary.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@DBOS.step(name="secondshift.mark_run_started")
|
|
95
|
+
@transaction.atomic
|
|
96
|
+
def mark_run_started_step(run_id: int) -> bool:
|
|
97
|
+
"""Start a queued run. Raises RunNotReadyError before any work is done."""
|
|
98
|
+
run = get_run(run_id, "task")
|
|
99
|
+
if run.job_id is None:
|
|
100
|
+
repository = lock_repository(run.task.repository_id)
|
|
101
|
+
run.refresh_from_db()
|
|
102
|
+
if problem := task_admission_problem(run, repository):
|
|
103
|
+
mark_run_terminal(
|
|
104
|
+
run.pk,
|
|
105
|
+
Run.Status.FAILED,
|
|
106
|
+
problem,
|
|
107
|
+
workflow_status=WorkflowStatus.NOT_ADMITTED,
|
|
108
|
+
)
|
|
109
|
+
return False
|
|
110
|
+
if run.status == Run.Status.RUNNING and run.dbos_workflow_id == DBOS.workflow_id:
|
|
111
|
+
# The application commit may precede DBOS persisting this step's result.
|
|
112
|
+
return True
|
|
113
|
+
plan = run.plan
|
|
114
|
+
if run.status == Run.Status.QUEUED and not plan.ready:
|
|
115
|
+
# A scheduled tick creates the run even when the job lost its access.
|
|
116
|
+
problems = execution_problems(run.repository, plan)
|
|
117
|
+
raise RunNotReadyError(
|
|
118
|
+
" ".join(problems) or "The frozen execution plan is not ready."
|
|
119
|
+
)
|
|
120
|
+
started = mark_running(run_id, DBOS.workflow_id or run.dbos_workflow_id)
|
|
121
|
+
if started:
|
|
122
|
+
add_event(run_id, "Run started.")
|
|
123
|
+
return bool(started)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@DBOS.step(name="secondshift.record_run_event")
|
|
127
|
+
def record_run_event_step(run_id: int, message: str) -> None:
|
|
128
|
+
add_event(run_id, message)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@DBOS.step(
|
|
132
|
+
name="secondshift.prepare_source",
|
|
133
|
+
retries_allowed=True,
|
|
134
|
+
max_attempts=3,
|
|
135
|
+
)
|
|
136
|
+
def prepare_source_step(run_id: int) -> None:
|
|
137
|
+
"""Build the snapshot of the run's commit, fetching it if needed."""
|
|
138
|
+
run = get_run(run_id, "job__repository", "task__repository")
|
|
139
|
+
repository = run.repository
|
|
140
|
+
if repository.source_type != Repository.SourceType.LOCAL:
|
|
141
|
+
# Lost access stops the run here, even if the snapshot is cached.
|
|
142
|
+
github_access(repository)
|
|
143
|
+
snapshot = source_snapshot(repository, run.commit_sha)
|
|
144
|
+
add_event(
|
|
145
|
+
run.pk,
|
|
146
|
+
f"Checked out the repository at {run.commit_sha[:12]} "
|
|
147
|
+
f"({snapshot.size_bytes / (1024 * 1024):.1f} MiB compressed).",
|
|
148
|
+
)
|
|
149
|
+
job = run.plan.job
|
|
150
|
+
if source := job.skill_source:
|
|
151
|
+
skill_snapshot(source.repository, job.skill_path, source.commit_sha)
|
|
152
|
+
add_event(
|
|
153
|
+
run.pk,
|
|
154
|
+
f"Fetched the skill {job.skill_path} from {source.repository} "
|
|
155
|
+
f"at {source.commit_sha[:12]}. The sandbox reads it at "
|
|
156
|
+
f"{SANDBOX_SKILL_DIR}.",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@DBOS.step(
|
|
161
|
+
name="secondshift.start_runner_session",
|
|
162
|
+
retries_allowed=True,
|
|
163
|
+
max_attempts=3,
|
|
164
|
+
)
|
|
165
|
+
def start_runner_session_step(run_id: int) -> str:
|
|
166
|
+
run = get_run(run_id, "job__repository", "task__repository")
|
|
167
|
+
plan = run.plan
|
|
168
|
+
if plan.repository.config_sha != run.commit_sha:
|
|
169
|
+
raise RuntimeError("The run commit does not match its execution plan.")
|
|
170
|
+
snapshot = source_snapshot(run.repository, run.commit_sha)
|
|
171
|
+
client = get_runner_client()
|
|
172
|
+
client.upload_source(snapshot.sha256, snapshot.path)
|
|
173
|
+
with tempfile.TemporaryDirectory(prefix="secondshift-history-") as folder:
|
|
174
|
+
archive = Path(folder) / "history.tar.gz"
|
|
175
|
+
history = pack_earlier_files(run, archive)
|
|
176
|
+
if history is not None:
|
|
177
|
+
client.upload_source(history.sha256, archive)
|
|
178
|
+
inputs = run.inputs.model_copy(update={"history_files": history.files})
|
|
179
|
+
update_run(run.pk, input_snapshot=inputs.stored())
|
|
180
|
+
skill_sha256 = ""
|
|
181
|
+
if source := plan.job.skill_source:
|
|
182
|
+
skill = skill_snapshot(
|
|
183
|
+
source.repository, plan.job.skill_path, source.commit_sha
|
|
184
|
+
)
|
|
185
|
+
client.upload_source(skill.sha256, skill.path)
|
|
186
|
+
skill_sha256 = skill.sha256
|
|
187
|
+
session_id = client.start(
|
|
188
|
+
RunnerSessionSpec(
|
|
189
|
+
run_id=run.pk,
|
|
190
|
+
session_key=run.runner_session_key.hex,
|
|
191
|
+
repository=plan.repository.full_name,
|
|
192
|
+
commit_sha=run.commit_sha,
|
|
193
|
+
image=plan.runner.image,
|
|
194
|
+
source_sha256=snapshot.sha256,
|
|
195
|
+
collect_changes=plan.runner.collect_changes,
|
|
196
|
+
history_sha256=history.sha256 if history is not None else "",
|
|
197
|
+
skill_sha256=skill_sha256,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
update_run(run.pk, sandbox_runtime=RunSandbox.ISOLATED_RUNNER)
|
|
201
|
+
add_event(run.pk, "Sandbox started without network access.")
|
|
202
|
+
return session_id
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@DBOS.step(
|
|
206
|
+
name="secondshift.collect_run_outputs",
|
|
207
|
+
retries_allowed=True,
|
|
208
|
+
max_attempts=3,
|
|
209
|
+
)
|
|
210
|
+
def collect_outputs_step(run_id: int, session_id: str) -> None:
|
|
211
|
+
"""Keep the agent's file changes and saved files before the sandbox goes."""
|
|
212
|
+
run = get_run(run_id, "job", "task__repository")
|
|
213
|
+
with tempfile.TemporaryDirectory(prefix="secondshift-collection-") as folder:
|
|
214
|
+
archive = Path(folder) / "collection.tar.gz"
|
|
215
|
+
get_runner_client().collect(session_id, archive)
|
|
216
|
+
outputs = store_collection(run, archive)
|
|
217
|
+
update_run(run_id, outputs=outputs.stored())
|
|
218
|
+
if message := collection_summary(outputs):
|
|
219
|
+
add_event(run_id, message)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@DBOS.step(name="secondshift.record_collection_failure")
|
|
223
|
+
def record_collection_failure_step(run_id: int) -> None:
|
|
224
|
+
add_event(
|
|
225
|
+
run_id,
|
|
226
|
+
"The run's file changes and saved files could not be collected.",
|
|
227
|
+
level="warning",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def collect_outputs(run_id: int, session_id: str) -> None:
|
|
232
|
+
# The report is already stored, so a failure here never changes the outcome.
|
|
233
|
+
try:
|
|
234
|
+
collect_outputs_step(run_id, session_id)
|
|
235
|
+
except Exception:
|
|
236
|
+
logger.warning("could not collect outputs for run %s", run_id)
|
|
237
|
+
record_collection_failure_step(run_id)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@DBOS.step(
|
|
241
|
+
name="secondshift.stop_runner_session",
|
|
242
|
+
retries_allowed=True,
|
|
243
|
+
max_attempts=3,
|
|
244
|
+
)
|
|
245
|
+
def stop_runner_session_step(session_id: str) -> None:
|
|
246
|
+
get_runner_client().stop(session_id)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@DBOS.step(name="secondshift.finish_run")
|
|
250
|
+
@transaction.atomic
|
|
251
|
+
def finish_run_step(run_id: int, stored: dict[str, Any]) -> None:
|
|
252
|
+
result = AgentResult.model_validate(stored)
|
|
253
|
+
status = Run.Status.SUCCEEDED if result.succeeded else Run.Status.FAILED
|
|
254
|
+
fields: dict[str, object] = {"exit_code": result.exit_code}
|
|
255
|
+
if output := result.output:
|
|
256
|
+
fields |= {
|
|
257
|
+
"output_path": output.path,
|
|
258
|
+
"output_size_bytes": output.size_bytes,
|
|
259
|
+
"output_sha256": output.sha256,
|
|
260
|
+
"output_truncated": output.truncated,
|
|
261
|
+
"output_excerpt": read_output_excerpt(output.path),
|
|
262
|
+
}
|
|
263
|
+
run = finish_run(
|
|
264
|
+
run_id,
|
|
265
|
+
status,
|
|
266
|
+
result.summary,
|
|
267
|
+
only_from={Run.Status.RUNNING},
|
|
268
|
+
fields=fields,
|
|
269
|
+
failure=result.failure,
|
|
270
|
+
reason=result.reason,
|
|
271
|
+
)
|
|
272
|
+
if run is None:
|
|
273
|
+
# A replay, or the run was canceled while it ran. Its result stands.
|
|
274
|
+
logger.info("run %s had already finished", run_id)
|
|
275
|
+
return
|
|
276
|
+
if reached := result.details.get("budget_reached"):
|
|
277
|
+
add_event(
|
|
278
|
+
run.pk,
|
|
279
|
+
budget_reached_message(Budget(reached)),
|
|
280
|
+
level="warning",
|
|
281
|
+
metadata=result.details,
|
|
282
|
+
)
|
|
283
|
+
add_event(
|
|
284
|
+
run.pk,
|
|
285
|
+
"Run succeeded. The report is stored as the run's output."
|
|
286
|
+
if result.succeeded
|
|
287
|
+
else result.summary,
|
|
288
|
+
level="info" if result.succeeded else "error",
|
|
289
|
+
metadata=result.details,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class RetryableDeliveryError(Exception):
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@DBOS.step(name="secondshift.notification_targets")
|
|
298
|
+
def notification_targets_step(run_id: int) -> list[int]:
|
|
299
|
+
return notification_targets(get_run(run_id, "job"))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@DBOS.step(
|
|
303
|
+
name="secondshift.deliver_run_notification",
|
|
304
|
+
retries_allowed=True,
|
|
305
|
+
max_attempts=3,
|
|
306
|
+
)
|
|
307
|
+
def deliver_run_notification_step(run_id: int, connection_id: int) -> None:
|
|
308
|
+
# One step per target, so a retry never re-sends to targets that succeeded.
|
|
309
|
+
run = get_run(run_id, "job__repository", "task__repository")
|
|
310
|
+
connection = get_connection(connection_id)
|
|
311
|
+
delivery = notification_delivery(run, connection)
|
|
312
|
+
if delivery.status == RunDelivery.Status.DELIVERED:
|
|
313
|
+
return
|
|
314
|
+
# A grant revoked or a connection disconnected since planning stops delivery,
|
|
315
|
+
# including when a failed delivery is retried later.
|
|
316
|
+
if connection_id not in notification_targets(run):
|
|
317
|
+
finish_delivery(
|
|
318
|
+
delivery, RunDelivery.Status.SKIPPED, "No longer approved.", announce=False
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
result = deliver_webhook(connection, run_report_payload(run))
|
|
322
|
+
delivery.attempts += 1
|
|
323
|
+
delivery.status_code = result.status_code
|
|
324
|
+
if result.retryable:
|
|
325
|
+
finish_delivery(
|
|
326
|
+
delivery, RunDelivery.Status.RETRYING, result.message, announce=False
|
|
327
|
+
)
|
|
328
|
+
raise RetryableDeliveryError(result.message)
|
|
329
|
+
finish_delivery(
|
|
330
|
+
delivery,
|
|
331
|
+
RunDelivery.Status.DELIVERED if result.ok else RunDelivery.Status.FAILED,
|
|
332
|
+
result.message,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@DBOS.step(name="secondshift.record_notification_failure")
|
|
337
|
+
def record_notification_failure_step(run_id: int, connection_id: int) -> None:
|
|
338
|
+
binding = connection_binding(connection_id)
|
|
339
|
+
# The last attempt's message stays, since it says why delivery failed.
|
|
340
|
+
fail_notification_delivery(run_id, connection_id, timezone.now())
|
|
341
|
+
add_event(
|
|
342
|
+
run_id,
|
|
343
|
+
f"Notification to {binding or 'a connection'} failed after retries.",
|
|
344
|
+
level="warning",
|
|
345
|
+
metadata={"connection_id": connection_id},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def deliver_notification(run_id: int, connection_id: int) -> None:
|
|
350
|
+
try:
|
|
351
|
+
deliver_run_notification_step(run_id, connection_id)
|
|
352
|
+
except Exception:
|
|
353
|
+
logger.warning(
|
|
354
|
+
"notification for run %s to connection %s failed",
|
|
355
|
+
run_id,
|
|
356
|
+
connection_id,
|
|
357
|
+
)
|
|
358
|
+
record_notification_failure_step(run_id, connection_id)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@DBOS.step(name="secondshift.publish_pull_request")
|
|
362
|
+
def publish_pull_request_step(run_id: int) -> None:
|
|
363
|
+
publish_run(run_id)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@DBOS.workflow(name=RETRY_PULL_REQUEST_WORKFLOW)
|
|
367
|
+
def retry_pull_request_workflow(run_id: int) -> None:
|
|
368
|
+
publish_pull_request_step(run_id)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@DBOS.step(name="secondshift.open_issues")
|
|
372
|
+
def open_issues_step(run_id: int) -> None:
|
|
373
|
+
open_run_issues(run_id)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@DBOS.workflow(name=RETRY_ISSUES_WORKFLOW)
|
|
377
|
+
def retry_issues_workflow(run_id: int) -> None:
|
|
378
|
+
open_issues_step(run_id)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def deliver(run_id: int) -> None:
|
|
382
|
+
"""Publish first, so the notification can link to the pull request and issues."""
|
|
383
|
+
try:
|
|
384
|
+
publish_pull_request_step(run_id)
|
|
385
|
+
except Exception:
|
|
386
|
+
logger.exception("could not publish run %s", run_id)
|
|
387
|
+
try:
|
|
388
|
+
open_issues_step(run_id)
|
|
389
|
+
except Exception:
|
|
390
|
+
logger.exception("could not open issues for run %s", run_id)
|
|
391
|
+
notify(run_id)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def notify(run_id: int) -> None:
|
|
395
|
+
# Delivery problems are recorded on the run but never change its outcome.
|
|
396
|
+
for connection_id in notification_targets_step(run_id):
|
|
397
|
+
deliver_notification(run_id, connection_id)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@DBOS.workflow(name=RETRY_NOTIFICATION_WORKFLOW)
|
|
401
|
+
def retry_notification_workflow(run_id: int, connection_id: int) -> None:
|
|
402
|
+
deliver_notification(run_id, connection_id)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@DBOS.step(name="secondshift.fail_run")
|
|
406
|
+
def fail_run_step(
|
|
407
|
+
run_id: int, summary: str, failure: str, reason: str, details: dict[str, object]
|
|
408
|
+
) -> None:
|
|
409
|
+
mark_run_terminal(
|
|
410
|
+
run_id,
|
|
411
|
+
Run.Status.FAILED,
|
|
412
|
+
summary,
|
|
413
|
+
workflow_status=WorkflowStatus.ERROR,
|
|
414
|
+
metadata=details,
|
|
415
|
+
failure=RunFailure(failure),
|
|
416
|
+
reason=reason,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def execute_run(run_id: int) -> None:
|
|
421
|
+
session_id: str | None = None
|
|
422
|
+
deadline: datetime | None = None
|
|
423
|
+
timeout: StageFailure | None = None
|
|
424
|
+
stage = RunFailure.STARTING
|
|
425
|
+
try:
|
|
426
|
+
if not mark_run_started_step(run_id):
|
|
427
|
+
return
|
|
428
|
+
stage = RunFailure.CHECKOUT
|
|
429
|
+
prepare_source_step(run_id)
|
|
430
|
+
stage = RunFailure.SANDBOX
|
|
431
|
+
session_id = start_runner_session_step(run_id)
|
|
432
|
+
stage = RunFailure.SKILL
|
|
433
|
+
run = get_run(run_id, "job")
|
|
434
|
+
record_run_event_step(
|
|
435
|
+
run_id,
|
|
436
|
+
f"Running {run.plan.job.skill_path or 'the submitted task'}.",
|
|
437
|
+
)
|
|
438
|
+
deadline = run_deadline(run)
|
|
439
|
+
timeout = timeout_failure(run)
|
|
440
|
+
result = execute_agent_run(run, session_id)
|
|
441
|
+
stage = RunFailure.FINISHING
|
|
442
|
+
collect_outputs(run_id, session_id)
|
|
443
|
+
stop_runner_session_step(session_id)
|
|
444
|
+
session_id = None
|
|
445
|
+
finish_run_step(run_id, result.model_dump(mode="json"))
|
|
446
|
+
except Exception as error:
|
|
447
|
+
if session_id is not None:
|
|
448
|
+
try:
|
|
449
|
+
stop_runner_session_step(session_id)
|
|
450
|
+
except Exception:
|
|
451
|
+
logger.exception("failed to stop runner session %s", session_id)
|
|
452
|
+
details = failure_metadata(stage, error)
|
|
453
|
+
timed_out = deadline is not None and timezone.now() >= deadline
|
|
454
|
+
if stage == RunFailure.SKILL and timeout is not None and timed_out:
|
|
455
|
+
# The skill stopped after its time limit, so the error is the model
|
|
456
|
+
# call or tool call that hit the deadline.
|
|
457
|
+
failure = timeout
|
|
458
|
+
details["timed_out"] = True
|
|
459
|
+
else:
|
|
460
|
+
failure = stage_failure(stage, error)
|
|
461
|
+
fail_run_step(run_id, failure.summary, failure.kind, failure.reason, details)
|
|
462
|
+
# A failed run notifies too, so a scheduled job never fails silently.
|
|
463
|
+
try:
|
|
464
|
+
deliver(run_id)
|
|
465
|
+
except Exception:
|
|
466
|
+
logger.exception("could not notify about failed run %s", run_id)
|
|
467
|
+
raise
|
|
468
|
+
deliver(run_id)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@DBOS.workflow(name=EXECUTE_RUN_WORKFLOW)
|
|
472
|
+
def execute_run_workflow(run_id: int) -> None:
|
|
473
|
+
execute_run(run_id)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@DBOS.workflow(name=SCHEDULED_JOB_WORKFLOW)
|
|
477
|
+
def scheduled_job_workflow(scheduled_time: datetime, context: Any) -> None:
|
|
478
|
+
run_id = create_scheduled_run_step(scheduled_time, context)
|
|
479
|
+
if run_id is not None:
|
|
480
|
+
execute_run(run_id)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""What the sandbox left behind after a run, as stored in Run.outputs."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Record(BaseModel):
|
|
7
|
+
model_config = ConfigDict(frozen=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SavedFile(Record):
|
|
11
|
+
"""A file the agent saved for people to download."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
path: str
|
|
15
|
+
size_bytes: int
|
|
16
|
+
sha256: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SkippedFile(Record):
|
|
20
|
+
name: str
|
|
21
|
+
reason: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Changes(Record):
|
|
25
|
+
"""The agent's changes to the repository. `path` is empty when not kept."""
|
|
26
|
+
|
|
27
|
+
file_count: int = 0
|
|
28
|
+
files: tuple[str, ...] = ()
|
|
29
|
+
too_large: bool = False
|
|
30
|
+
path: str = ""
|
|
31
|
+
size_bytes: int | None = None
|
|
32
|
+
sha256: str = ""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def kept_file_count(self) -> int:
|
|
36
|
+
return self.file_count if self.path else 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RunOutputs(Record):
|
|
40
|
+
changes: Changes = Changes()
|
|
41
|
+
files: tuple[SavedFile, ...] = ()
|
|
42
|
+
skipped: tuple[SkippedFile, ...] = ()
|
|
43
|
+
|
|
44
|
+
def saved_file(self, name: str) -> SavedFile | None:
|
|
45
|
+
return next((item for item in self.files if item.name == name), None)
|
|
46
|
+
|
|
47
|
+
def stored_paths(self) -> list[str]:
|
|
48
|
+
paths = [item.path for item in self.files] + [self.changes.path]
|
|
49
|
+
return [path for path in paths if path]
|
|
50
|
+
|
|
51
|
+
def stored(self) -> dict:
|
|
52
|
+
return self.model_dump(mode="json")
|