@wardby/cli 0.5.3 → 0.6.0
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.
- package/.env.example +33 -0
- package/README.md +25 -14
- package/dist/claude-coding-worker/context.d.ts +22 -0
- package/dist/claude-coding-worker/context.js +92 -0
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +38 -5
- package/dist/claude-coding-worker/sdk.js +13 -4
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-native-mode.d.ts +9 -0
- package/dist/cli-native-mode.js +20 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +141 -5
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/profile.d.ts +12 -0
- package/dist/coding/profile.js +6 -0
- package/dist/coding/protocol.d.ts +104 -0
- package/dist/coding/protocol.js +87 -3
- package/dist/coding-worker/artifact.d.ts +8 -0
- package/dist/coding-worker/artifact.js +2 -2
- package/dist/coding-worker/driver.js +2 -0
- package/dist/coding-worker/sdk.d.ts +6 -0
- package/dist/coding-worker/sdk.js +1 -0
- package/dist/coding-worker/skills.d.ts +23 -0
- package/dist/coding-worker/skills.js +117 -0
- package/dist/coding-worker/types.d.ts +2 -0
- package/dist/config/github-allowed-accounts.d.ts +15 -0
- package/dist/config/github-allowed-accounts.js +36 -0
- package/dist/config/low-power.d.ts +12 -0
- package/dist/config/low-power.js +18 -0
- package/dist/config/mobile.d.ts +12 -0
- package/dist/config/mobile.js +18 -0
- package/dist/config/providers.d.ts +53 -0
- package/dist/config/providers.js +92 -6
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/dispatch.d.ts +11 -0
- package/dist/core/dispatch.js +36 -1
- package/dist/core/engine-native.js +9 -5
- package/dist/core/host-events.d.ts +49 -3
- package/dist/core/host-events.js +289 -15
- package/dist/core/issue-bridge.js +42 -2
- package/dist/core/issue-events.js +12 -1
- package/dist/core/issue-status.js +14 -3
- package/dist/core/merge-order-check.d.ts +129 -0
- package/dist/core/merge-order-check.js +336 -0
- package/dist/core/notification-dispatcher.d.ts +70 -0
- package/dist/core/notification-dispatcher.js +368 -0
- package/dist/core/notification-templates.d.ts +25 -0
- package/dist/core/notification-templates.js +105 -0
- package/dist/core/notifications.d.ts +10 -0
- package/dist/core/notifications.js +38 -0
- package/dist/core/pg-auth.d.ts +22 -0
- package/dist/core/pg-auth.js +152 -0
- package/dist/core/power-composition.d.ts +16 -0
- package/dist/core/power-composition.js +37 -0
- package/dist/core/power-idle.d.ts +14 -0
- package/dist/core/power-idle.js +30 -0
- package/dist/core/power.d.ts +71 -0
- package/dist/core/power.js +202 -0
- package/dist/core/reconciler.d.ts +4 -2
- package/dist/core/reconciler.js +29 -5
- package/dist/core/related-pull-requests.d.ts +48 -4
- package/dist/core/related-pull-requests.js +113 -11
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +37 -0
- package/dist/core/review-fix.js +207 -6
- package/dist/core/review-host-tools.d.ts +20 -0
- package/dist/core/review-host-tools.js +58 -2
- package/dist/core/runner.d.ts +336 -3
- package/dist/core/runner.js +894 -317
- package/dist/core/secret-broker-config.d.ts +103 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/workflow-event-recorder.d.ts +14 -0
- package/dist/core/workflow-event-recorder.js +120 -0
- package/dist/core/workflow-events.d.ts +82 -0
- package/dist/core/workflow-events.js +44 -0
- package/dist/core/workflow-run-events.d.ts +16 -0
- package/dist/core/workflow-run-events.js +40 -0
- package/dist/generated/prisma/browser.d.ts +58 -0
- package/dist/generated/prisma/client.d.ts +58 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
- package/dist/generated/prisma/enums.d.ts +27 -0
- package/dist/generated/prisma/enums.js +23 -0
- package/dist/generated/prisma/internal/class.d.ts +88 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
- package/dist/generated/prisma/models/Agent.d.ts +360 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
- package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
- package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
- package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
- package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
- package/dist/generated/prisma/models/NotificationChannel.js +1 -0
- package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
- package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
- package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
- package/dist/generated/prisma/models/NotificationThread.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +826 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/Secret.d.ts +37 -1
- package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
- package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
- package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
- package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
- package/dist/generated/prisma/models.d.ts +8 -0
- package/dist/help/build.js +4 -2
- package/dist/help/catalog.d.ts +1 -1
- package/dist/help/catalog.js +31 -6
- package/dist/help-index.json +1363 -176
- package/dist/import/create.js +17 -0
- package/dist/import/neutral-schema.d.ts +6 -2
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/auth/repo-authorization.js +14 -0
- package/dist/mcp/auth/resource-server.d.ts +2 -1
- package/dist/mcp/auth/resource-server.js +7 -2
- package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +19 -6
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +23 -1
- package/dist/mcp/host-events/github-ingress.js +68 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +59 -10
- package/dist/mcp/tools/agents.js +82 -11
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +7 -2
- package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
- package/dist/mcp/tools/secret-broker-compat.js +23 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
- package/dist/mcp/tools/secret-elicitation-form.js +200 -10
- package/dist/mcp/tools/secret-elicitation-server.js +8 -1
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
- package/dist/mcp/tools/secret-elicitation.js +110 -12
- package/dist/mcp/tools/secrets.js +184 -7
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +22 -2
- package/dist/native-worker/composition.d.ts +23 -0
- package/dist/native-worker/composition.js +77 -0
- package/dist/native-worker/docker-isolation.d.ts +65 -0
- package/dist/native-worker/docker-isolation.js +183 -0
- package/dist/native-worker/docker-launcher.d.ts +74 -0
- package/dist/native-worker/docker-launcher.js +237 -0
- package/dist/native-worker/gateway-server.d.ts +39 -0
- package/dist/native-worker/gateway-server.js +315 -0
- package/dist/native-worker/gateway.d.ts +44 -0
- package/dist/native-worker/gateway.js +197 -0
- package/dist/native-worker/http-server.d.ts +22 -0
- package/dist/native-worker/http-server.js +109 -0
- package/dist/native-worker/http-transport.d.ts +17 -0
- package/dist/native-worker/http-transport.js +88 -0
- package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
- package/dist/native-worker/kubernetes-isolation.js +218 -0
- package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
- package/dist/native-worker/kubernetes-launcher.js +335 -0
- package/dist/native-worker/launch.d.ts +45 -0
- package/dist/native-worker/launch.js +44 -0
- package/dist/native-worker/ledger.d.ts +109 -0
- package/dist/native-worker/ledger.js +192 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +49 -0
- package/dist/native-worker/main.d.ts +7 -0
- package/dist/native-worker/main.js +74 -0
- package/dist/native-worker/protocol.d.ts +744 -0
- package/dist/native-worker/protocol.js +200 -0
- package/dist/native-worker/sandbox-executor.d.ts +68 -0
- package/dist/native-worker/sandbox-executor.js +210 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +188 -0
- package/dist/native-worker/warm-delivery.d.ts +16 -0
- package/dist/native-worker/warm-delivery.js +33 -0
- package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
- package/dist/native-worker/warm-pool-ledger.js +73 -0
- package/dist/native-worker/warm-pool.d.ts +107 -0
- package/dist/native-worker/warm-pool.js +283 -0
- package/dist/native-worker/worker.d.ts +17 -0
- package/dist/native-worker/worker.js +115 -0
- package/dist/providers/auth/authorization-server.d.ts +3 -0
- package/dist/providers/auth/index.js +2 -0
- package/dist/providers/auth/self-hosted.d.ts +13 -1
- package/dist/providers/auth/self-hosted.js +54 -4
- package/dist/providers/chat/fake.d.ts +46 -0
- package/dist/providers/chat/fake.js +39 -0
- package/dist/providers/chat/index.d.ts +4 -0
- package/dist/providers/chat/index.js +8 -0
- package/dist/providers/chat/slack.d.ts +37 -0
- package/dist/providers/chat/slack.js +105 -0
- package/dist/providers/chat/types.d.ts +42 -0
- package/dist/providers/chat/types.js +19 -0
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/engine/types.js +22 -0
- package/dist/providers/executor/composition.d.ts +24 -1
- package/dist/providers/executor/composition.js +39 -6
- package/dist/providers/executor/container.d.ts +24 -1
- package/dist/providers/executor/container.js +101 -11
- package/dist/providers/executor/dbos.d.ts +42 -0
- package/dist/providers/executor/dbos.js +118 -12
- package/dist/providers/executor/deferred.d.ts +56 -0
- package/dist/providers/executor/deferred.js +119 -0
- package/dist/providers/executor/routing.d.ts +37 -10
- package/dist/providers/executor/routing.js +73 -32
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/jobs/docker.js +3 -3
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
- package/dist/providers/jobs/kubernetes-api.js +7 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +52 -3
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +6 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
- package/dist/providers/jobs/kubernetes-platform.js +38 -1
- package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-preflight.js +4 -2
- package/dist/providers/jobs/kubernetes.d.ts +36 -1
- package/dist/providers/jobs/kubernetes.js +74 -4
- package/dist/providers/jobs/metadata-probes.d.ts +11 -0
- package/dist/providers/jobs/metadata-probes.js +13 -0
- package/dist/providers/jobs/types.d.ts +9 -0
- package/dist/providers/review-host/github.d.ts +11 -1
- package/dist/providers/review-host/github.js +50 -0
- package/dist/providers/review-host/types.d.ts +23 -0
- package/dist/providers/vcs/github.d.ts +32 -6
- package/dist/providers/vcs/github.js +74 -16
- package/dist/quickstart/coding-seed.d.ts +10 -3
- package/dist/quickstart/coding-seed.js +74 -3
- package/dist/quickstart/images.d.ts +2 -0
- package/dist/quickstart/images.js +7 -1
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +1 -1
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/brokered-fetch.d.ts +23 -0
- package/dist/sandbox/brokered-fetch.js +37 -0
- package/dist/sandbox/host-functions.d.ts +34 -0
- package/dist/sandbox/host-functions.js +144 -80
- package/dist/sandbox/prelude.js +7 -1
- package/dist/sandbox/run-in-sandbox.d.ts +19 -0
- package/dist/sandbox/run-in-sandbox.js +28 -10
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +3 -1
- package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
- package/dist/sandbox/secret-broker-sigv4.js +49 -0
- package/dist/sandbox/secret-broker.d.ts +34 -0
- package/dist/sandbox/secret-broker.js +162 -0
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/serve.js +18 -0
- package/dist/viewer/api-schema.d.ts +210 -14
- package/dist/viewer/api-schema.js +22 -0
- package/dist/viewer/build-schemas.js +4 -1
- package/dist/viewer/event-bus.d.ts +6 -0
- package/dist/viewer/event-bus.js +16 -7
- package/dist/viewer/graph.d.ts +5 -2
- package/dist/viewer/graph.js +7 -2
- package/dist/viewer/http.d.ts +0 -1
- package/dist/viewer/http.js +2 -2
- package/dist/viewer/infra.js +25 -2
- package/dist/viewer/limits.d.ts +4 -0
- package/dist/viewer/limits.js +4 -0
- package/dist/viewer/openapi.d.ts +15 -0
- package/dist/viewer/openapi.js +172 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +57 -8
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +139 -5
- package/docs/coding-agent-setup.md +113 -5
- package/docs/coding-packages.md +4 -2
- package/docs/coding-services.md +3 -1
- package/docs/coding-worker-byo-images.md +198 -6
- package/docs/coding-worker-isolation.md +68 -14
- package/docs/getting-started-gke.md +225 -9
- package/docs/getting-started-identity-provider.md +21 -10
- package/docs/getting-started.md +5 -4
- package/docs/jira-agents.md +5 -1
- package/docs/native-sandbox.md +635 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +113 -12
- package/docs/slack-notifications.md +252 -0
- package/docs/tool-secrets.md +252 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +6 -2
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +1 -1
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +3 -6
- package/help/code-review-agents.md +68 -6
- package/help/deploy-gke.md +8 -1
- package/help/deployment-targets.md +25 -14
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/github-account-not-allowed.md +25 -0
- package/help/errors/low-power-disabled.md +29 -0
- package/help/errors/low-power-scale-forbidden.md +35 -0
- package/help/errors/low-power-wake-timeout.md +30 -0
- package/help/errors/mobile-disabled.md +31 -0
- package/help/errors/native-sandbox-capacity.md +22 -0
- package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
- package/help/errors/native-sandbox-docker-failed.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
- package/help/errors/native-sandbox-image-not-pinned.md +19 -0
- package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
- package/help/errors/native-sandbox-network-unenforced.md +22 -0
- package/help/errors/native-sandbox-requires-catalog.md +31 -0
- package/help/errors/native-sandbox-unavailable.md +19 -0
- package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +20 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/secret-broker-body-invalid.md +26 -0
- package/help/errors/secret-broker-changed.md +20 -0
- package/help/errors/secret-broker-config-invalid.md +30 -0
- package/help/errors/secret-broker-conflict.md +27 -0
- package/help/errors/secret-broker-destination-denied.md +28 -0
- package/help/errors/secret-broker-value-invalid.md +28 -0
- package/help/errors/secret-brokered.md +24 -0
- package/help/errors/secret-not-brokered.md +25 -0
- package/help/errors/slack-auth-failed.md +48 -0
- package/help/errors/slack-channel-unreachable.md +39 -0
- package/help/errors/slack-not-configured.md +33 -0
- package/help/getting-started.md +4 -5
- package/help/github.md +18 -1
- package/help/identity-and-access.md +5 -1
- package/help/local-repositories.md +2 -3
- package/help/low-power-mode.md +125 -0
- package/help/merge-order-check.md +117 -0
- package/help/mobile-app.md +54 -0
- package/help/native-sandbox.md +136 -0
- package/help/observability.md +8 -0
- package/help/related-pull-requests.md +70 -9
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/review-fix-rounds.md +10 -0
- package/help/slack-notifications.md +62 -0
- package/help/troubleshooting/coding-workers.md +21 -0
- package/package.json +11 -4
- package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
- package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
- package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
- package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
- package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
- package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
- package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
- package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
- package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
- package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
- package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
- package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
- package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
- package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
- package/prisma/schema.prisma +362 -91
package/dist/core/runner.js
CHANGED
|
@@ -16,11 +16,15 @@
|
|
|
16
16
|
* down) lives inside the engine now — there is exactly one place that
|
|
17
17
|
* reasons about cost, not one here plus one in the engine.
|
|
18
18
|
*/
|
|
19
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
19
20
|
import { z } from "zod";
|
|
20
|
-
import { runStepInline } from "../providers/engine/types.js";
|
|
21
|
+
import { RunOwnershipLostError, runStepInline } from "../providers/engine/types.js";
|
|
21
22
|
import { isLlmEffort } from "../providers/llm/types.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { createPrivilegedHost } from "../sandbox/host-functions.js";
|
|
24
|
+
import { consoleRedactionValues } from "../sandbox/secret-broker.js";
|
|
25
|
+
import { runUserToolCall } from "../sandbox/user-tool.js";
|
|
26
|
+
import { runSandboxedEngine, SandboxRunCancelledError, } from "../native-worker/gateway.js";
|
|
27
|
+
import { NATIVE_WORKER_PROTOCOL_VERSION } from "../native-worker/protocol.js";
|
|
24
28
|
import { asStringArray, asPrefixMap } from "../sandbox/tool-capabilities.js";
|
|
25
29
|
import { scopeDatastore } from "../providers/datastore/scoped.js";
|
|
26
30
|
import { buildSecretsAccessor, scopeSecretsAccessor } from "./secrets.js";
|
|
@@ -47,8 +51,11 @@ import { ISSUE_TRACKER_TOOL_DEFS, ISSUE_TRACKER_TOOL_NAMES, handleIssueTrackerTo
|
|
|
47
51
|
import { completeIssueStatus } from "./issue-status.js";
|
|
48
52
|
import { fileIssue } from "./issue-dedupe.js";
|
|
49
53
|
import { fileSelfDefect } from "./self-defects.js";
|
|
54
|
+
import { emitRunFinishedEvents } from "./workflow-run-events.js";
|
|
50
55
|
import { startReviewFixAfterReview } from "./review-fix.js";
|
|
51
56
|
import { updateRelatedPullRequests } from "./related-pull-requests.js";
|
|
57
|
+
import { syncMergeOrderChecksAfterRun } from "./merge-order-check.js";
|
|
58
|
+
import { startDeferredForRequest } from "./host-events.js";
|
|
52
59
|
import { recordNativeModelUsage } from "./model-usage.js";
|
|
53
60
|
import { pinNativeRunPricing } from "./run-pricing.js";
|
|
54
61
|
import { RoutingLlmProvider } from "../providers/llm/routing.js";
|
|
@@ -115,20 +122,177 @@ function delegateToolDef(boundName, parallel) {
|
|
|
115
122
|
type: "string",
|
|
116
123
|
description: "Coding sub-agents only. The run id of a prior coding run whose branch/PR this dispatch should push a new commit onto, instead of opening a fresh one — use when the task is a revision to an existing PR (a plan/spec update, or a code-review follow-up), even across a different sub-agent than the one that opened it.",
|
|
117
124
|
},
|
|
125
|
+
mergeOrder: {
|
|
126
|
+
type: "integer",
|
|
127
|
+
minimum: 1,
|
|
128
|
+
maximum: 99,
|
|
129
|
+
description: "Coding sub-agents only. This repository's step in the merge order of the whole request (1 = merge first). " +
|
|
130
|
+
"Give dependencies lower numbers than the changes that use them (e.g. a service 1, the API that calls it 2, the client 3); " +
|
|
131
|
+
"equal numbers have no order between them. Set it whenever one change spans several repositories.",
|
|
132
|
+
},
|
|
118
133
|
},
|
|
119
134
|
required: ["task"],
|
|
120
135
|
additionalProperties: false,
|
|
121
136
|
},
|
|
122
137
|
};
|
|
123
138
|
}
|
|
139
|
+
/** Every mergeOrder validation failure (wrong type, non-integer, or out of range) names the same bound. */
|
|
140
|
+
const MERGE_ORDER_RANGE_MESSAGE = "mergeOrder must be an integer from 1 to 99.";
|
|
124
141
|
const DelegateArgs = z
|
|
125
142
|
.object({
|
|
126
143
|
task: z.string(),
|
|
127
144
|
datastoreRef: z.object({ name: z.string(), key: z.string() }).strict().optional(),
|
|
128
145
|
grantParentMemoryKeys: z.array(z.string()).optional(),
|
|
129
146
|
continuePriorRun: z.string().optional(),
|
|
147
|
+
mergeOrder: z
|
|
148
|
+
.number({ invalid_type_error: MERGE_ORDER_RANGE_MESSAGE })
|
|
149
|
+
.int(MERGE_ORDER_RANGE_MESSAGE)
|
|
150
|
+
.min(1, MERGE_ORDER_RANGE_MESSAGE)
|
|
151
|
+
.max(99, MERGE_ORDER_RANGE_MESSAGE)
|
|
152
|
+
.optional(),
|
|
130
153
|
})
|
|
131
154
|
.strict();
|
|
155
|
+
/** The tool result note for a native child's delegation when the caller passed mergeOrder: it does not apply. */
|
|
156
|
+
const MERGE_ORDER_IGNORED_NOTE = "mergeOrder applies only to coding sub-agents; ignored";
|
|
157
|
+
/** A delegate_to_* call's arguments, or the tool result refusing them. */
|
|
158
|
+
export function parseDelegateArgs(argsJson) {
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(argsJson || "{}");
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
refusal: JSON.stringify({
|
|
166
|
+
error: "invalid_arguments_json",
|
|
167
|
+
message: err instanceof Error ? err.message : String(err),
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return { args: DelegateArgs.parse(parsed) };
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (err instanceof z.ZodError) {
|
|
176
|
+
return {
|
|
177
|
+
refusal: JSON.stringify({
|
|
178
|
+
error: "validation_failed",
|
|
179
|
+
message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* At most `limit` dispatches per run (default one: a classifier deciding plan-vs-implement
|
|
188
|
+
* commits to exactly one child), and never the same child twice, so a retry can't leave two
|
|
189
|
+
* children racing on the same task. A delegation waiting for budget holds its place.
|
|
190
|
+
*/
|
|
191
|
+
export function delegationLimitRefusal(input) {
|
|
192
|
+
const { priorChildAgentIds, waitingChildAgentIds, childAgentId, boundName, limit } = input;
|
|
193
|
+
if (priorChildAgentIds.includes(childAgentId) || waitingChildAgentIds.includes(childAgentId)) {
|
|
194
|
+
return JSON.stringify({
|
|
195
|
+
error: "already_dispatched",
|
|
196
|
+
message: `This run already delegated to "${boundName}"; a run delegates to each sub-agent at most once.`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (priorChildAgentIds.length < limit && priorChildAgentIds.length + waitingChildAgentIds.length >= limit) {
|
|
200
|
+
// Only a waiting delegation (parallelDelegations) holds the remaining place.
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
error: "already_dispatched",
|
|
203
|
+
message: `This run reached its limit of ${limit} delegations: ${priorChildAgentIds.length} made and ${waitingChildAgentIds.length} waiting for budget.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (priorChildAgentIds.length >= limit) {
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
error: "already_dispatched",
|
|
209
|
+
message: limit === 1
|
|
210
|
+
? "This run already delegated to a sub-agent; only one delegation is allowed per run."
|
|
211
|
+
: `This run already made ${limit} delegations, the most this agent allows per run.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const DELEGATION_CHILD_SELECT = {
|
|
217
|
+
id: true,
|
|
218
|
+
kind: true,
|
|
219
|
+
budgetGroupId: true,
|
|
220
|
+
budgetUsd: true,
|
|
221
|
+
ownerId: true,
|
|
222
|
+
nativeExecutionMode: true,
|
|
223
|
+
codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The live authorization of one delegation, and the child agent as it is now. Owners are re-read
|
|
227
|
+
* on every delegation (resource-sharing grants spec §3.4.4, N1): the child must have the parent's
|
|
228
|
+
* current owner, or that owner must still hold execute on the child. A revoked grant or a
|
|
229
|
+
* make_owner transfer stops the very next call.
|
|
230
|
+
*/
|
|
231
|
+
export async function authorizeDelegation(db, parentAgentId, edge, boundName, args) {
|
|
232
|
+
const [parentNow, childAgent] = await Promise.all([
|
|
233
|
+
db.agent.findUnique({ where: { id: parentAgentId }, select: { ownerId: true } }),
|
|
234
|
+
db.agent.findUniqueOrThrow({ where: { id: edge.childAgentId }, select: DELEGATION_CHILD_SELECT }),
|
|
235
|
+
]);
|
|
236
|
+
const parentOwnerId = parentNow?.ownerId ?? null;
|
|
237
|
+
const childOwnerId = childAgent.ownerId ?? null;
|
|
238
|
+
if (!parentNow ||
|
|
239
|
+
!(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
|
|
240
|
+
return {
|
|
241
|
+
refusal: JSON.stringify({
|
|
242
|
+
error: "subagent_not_authorized",
|
|
243
|
+
message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (parentOwnerId !== childOwnerId) {
|
|
248
|
+
// Across owners the edge carries execute and nothing more: no
|
|
249
|
+
// model-chosen memory grant, no continuation of another run's PR,
|
|
250
|
+
// and no task text the caller couldn't give the child directly --
|
|
251
|
+
// the trigger_agent rule (review I3): a coding child only with its
|
|
252
|
+
// owner's allowWebhookTaskOverride opt-in, a native child never
|
|
253
|
+
// (it runs its owner's fixed prompt; pass an empty task).
|
|
254
|
+
const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
|
|
255
|
+
? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
|
|
256
|
+
: args.continuePriorRun !== undefined
|
|
257
|
+
? "continuePriorRun is only allowed when the sub-agent has the same owner."
|
|
258
|
+
: childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
|
|
259
|
+
? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
|
|
260
|
+
: childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
|
|
261
|
+
? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
|
|
262
|
+
: null;
|
|
263
|
+
if (refusal)
|
|
264
|
+
return { refusal: JSON.stringify({ error: "cross_owner_not_allowed", message: refusal }) };
|
|
265
|
+
}
|
|
266
|
+
if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
|
|
267
|
+
return {
|
|
268
|
+
refusal: JSON.stringify({
|
|
269
|
+
error: "continuation_requires_coding_agent",
|
|
270
|
+
message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return { childAgent };
|
|
275
|
+
}
|
|
276
|
+
/** A native child's task text: the delegated task, with any datastore reference as a note. */
|
|
277
|
+
export function delegationTaskOverride(args) {
|
|
278
|
+
return args.datastoreRef
|
|
279
|
+
? `${args.task}\n\n[Referenced datastore: name="${args.datastoreRef.name}", key="${args.datastoreRef.key}" — use your datastore tools to read it.]`
|
|
280
|
+
: args.task.trim() !== ""
|
|
281
|
+
? args.task
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
284
|
+
/** The tool result a native child's terminal run reports to its parent. `note` surfaces e.g. an ignored mergeOrder. */
|
|
285
|
+
export function nativeChildResult(run, note) {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
status: run.status,
|
|
288
|
+
finalText: run.finalText,
|
|
289
|
+
costUsd: Number(run.costUsd),
|
|
290
|
+
tokensIn: run.tokensIn,
|
|
291
|
+
tokensOut: run.tokensOut,
|
|
292
|
+
...(run.error ? { error: run.error } : {}),
|
|
293
|
+
...(note ? { note } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
132
296
|
/**
|
|
133
297
|
* The composed review hosts, or undefined when there are none (no GitHub App
|
|
134
298
|
* configured yields an empty registry). Undefined means the run never touches
|
|
@@ -169,7 +333,8 @@ function toIssueProjectLink(row) {
|
|
|
169
333
|
* reaches a terminal state first must win. A conditional `updateMany` makes
|
|
170
334
|
* that a database-level CAS rather than a race — the loser's WHERE matches
|
|
171
335
|
* zero rows. It also stops a *later* attempt resurrecting a run the
|
|
172
|
-
* reconciler already reaped: a `lost` row stays `lost`.
|
|
336
|
+
* reconciler already reaped: a `lost` row stays `lost`. A step-level
|
|
337
|
+
* ownership loss writes nothing at all (RunOwnershipLostError).
|
|
173
338
|
*/
|
|
174
339
|
const DRIVABLE = ["pending", "running"];
|
|
175
340
|
/**
|
|
@@ -183,6 +348,15 @@ export class RunCancelledError extends Error {
|
|
|
183
348
|
this.name = "RunCancelledError";
|
|
184
349
|
}
|
|
185
350
|
}
|
|
351
|
+
/** Re-exported beside RunCancelledError: thrown by a step runner when another attempt owns the run. */
|
|
352
|
+
export { RunOwnershipLostError };
|
|
353
|
+
/**
|
|
354
|
+
* The one log line for an attempt that lost ownership of its run. Warn, not info: each one also
|
|
355
|
+
* means a discarded step's spend and an owner that stalled long enough to be adopted.
|
|
356
|
+
*/
|
|
357
|
+
function logRunOwnershipLost(runId, err) {
|
|
358
|
+
runnerLog.warn({ runId, step: err.step, event: "run_ownership_lost", discardedUsage: err.discardedUsage }, "run_ownership_lost: another execution owns this run; stopping without a terminal write (this attempt's last step is discarded)");
|
|
359
|
+
}
|
|
186
360
|
/**
|
|
187
361
|
* Terminal write + read-back. The write is conditional (see DRIVABLE), so
|
|
188
362
|
* the returned row is the run's real state — this attempt's result if it won
|
|
@@ -196,6 +370,15 @@ async function finishRunClaimed(db, runId, data) {
|
|
|
196
370
|
const updated = await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data });
|
|
197
371
|
return { run: await db.run.findUniqueOrThrow({ where: { id: runId } }), claimed: updated.count > 0 };
|
|
198
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Why a sandbox-mode native run is not executed here. The runner only ever
|
|
375
|
+
* executes native runs in the control plane; a run whose snapshot says
|
|
376
|
+
* `sandbox` reaches it only when no native sandbox executor is composed in
|
|
377
|
+
* (RoutingExecutor sends it to the native executor), and must fail closed
|
|
378
|
+
* rather than run unisolated.
|
|
379
|
+
*/
|
|
380
|
+
export const NATIVE_SANDBOX_NOT_CONFIGURED = "native_sandbox_unavailable: This agent runs with nativeExecutionMode=sandbox, but this deployment has no native " +
|
|
381
|
+
"sandbox executor configured, so the run was not started. Switch the agent back to control-plane mode to run it here.";
|
|
199
382
|
/**
|
|
200
383
|
* Why a coding run cannot start in this process: runs reach the runner only
|
|
201
384
|
* when no container executor is composed in (JOB_LAUNCHER=local, e.g. every
|
|
@@ -210,6 +393,12 @@ const CODING_EXECUTOR_NOT_CONFIGURED = "Coding agents need a container executor,
|
|
|
210
393
|
* write the terminal row, and the queue timeout only fires on a drain tick.
|
|
211
394
|
*/
|
|
212
395
|
export const CODING_CHILD_WAIT_GRACE_SEC = 60;
|
|
396
|
+
/**
|
|
397
|
+
* How long a parent waits for a sandbox-mode native child before stopping it. Native runs have
|
|
398
|
+
* no run timeout of their own; until the sandbox executor brings a per-run deadline, this bounds
|
|
399
|
+
* a parent left waiting on a child that never finishes.
|
|
400
|
+
*/
|
|
401
|
+
export const SANDBOX_CHILD_WAIT_SEC = 60 * 60;
|
|
213
402
|
/**
|
|
214
403
|
* What a coding sub-agent's run tells the parent run's model. A run refused
|
|
215
404
|
* over its services carries the host sentence (never the raw Run.error), so
|
|
@@ -306,40 +495,27 @@ export async function createRun(db, agentName, trigger = "manual") {
|
|
|
306
495
|
if (agent.kind === "coding") {
|
|
307
496
|
throw new Error(CODING_EXECUTOR_NOT_CONFIGURED);
|
|
308
497
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
|
|
313
|
-
// Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
|
|
314
|
-
return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
|
|
315
|
-
}
|
|
316
|
-
async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
317
|
-
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
318
|
-
if (!existingRun) {
|
|
319
|
-
throw new Error(`Unknown run "${runId}".`);
|
|
498
|
+
// Refused here, before any row: a foreground run executes inline, never in a sandbox.
|
|
499
|
+
if (agent.nativeExecutionMode === "sandbox") {
|
|
500
|
+
throw new Error(NATIVE_SANDBOX_NOT_CONFIGURED);
|
|
320
501
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const issueTrackers =
|
|
337
|
-
|
|
338
|
-
// or its budget group may have changed since first execution. The
|
|
339
|
-
// engine's control flow depends on budgetUsd and maxTurns, so they must
|
|
340
|
-
// be pinned to the values seen on first execution or the replay's step
|
|
341
|
-
// order diverges from the record.
|
|
342
|
-
const loadedOrUnavailable = await step("load", async () => {
|
|
502
|
+
return db.run.create({ data: { agentId: agent.id, trigger, nativeExecutionMode: agent.nativeExecutionMode } });
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The trusted load of a native run: the agent, its attached tools and their
|
|
506
|
+
* consented capabilities, the pinned catalog entry, the effective budget,
|
|
507
|
+
* sub-agent edges, and repository/issue links. Shared by in-process execution
|
|
508
|
+
* and the native sandbox gateway, which hands the worker this result.
|
|
509
|
+
*
|
|
510
|
+
* Pinned in one checkpointed step: on replay after a crash, the agent row
|
|
511
|
+
* or its budget group may have changed since first execution. The
|
|
512
|
+
* engine's control flow depends on budgetUsd and maxTurns, so they must
|
|
513
|
+
* be pinned to the values seen on first execution or the replay's step
|
|
514
|
+
* order diverges from the record.
|
|
515
|
+
*/
|
|
516
|
+
export async function loadNativeRun(options) {
|
|
517
|
+
const { runId, existingRun, providers, db, step, reviewHosts, issueTrackers } = options;
|
|
518
|
+
return step("load", async () => {
|
|
343
519
|
const agent = await db.agent.findUnique({ where: { id: existingRun.agentId } });
|
|
344
520
|
if (!agent) {
|
|
345
521
|
throw new Error(`Run "${runId}" references missing agent "${existingRun.agentId}".`);
|
|
@@ -486,53 +662,48 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
486
662
|
(err instanceof Error && err.message.startsWith("model_unavailable:"))) {
|
|
487
663
|
return { unavailable: err.message };
|
|
488
664
|
}
|
|
665
|
+
// Thrown out of executeRun ahead of its backstop, so logged here instead.
|
|
666
|
+
if (err instanceof RunOwnershipLostError)
|
|
667
|
+
logRunOwnershipLost(runId, err);
|
|
489
668
|
throw err;
|
|
490
669
|
});
|
|
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
|
-
// starts a fresh one, floored by the run's recorded fingerprint creates).
|
|
518
|
-
const issueCreationCounters = new Map();
|
|
519
|
-
// One per attempt: admits this run's delegations one at a time (see the delegate branch).
|
|
520
|
-
const delegationGate = createSerialGate();
|
|
521
|
-
// The children this attempt's delegations have running, and the sub-agents whose delegation is
|
|
522
|
-
// waiting for one of them to free budget (parallelDelegations): a waiting delegation keeps its
|
|
523
|
-
// place in the same-child and limit checks while the gate is open to the others.
|
|
524
|
-
const delegationSiblings = createDelegationSiblings();
|
|
525
|
-
const waitingDelegations = new Set();
|
|
526
|
-
const runSandboxTool = async (name, argsJson) => {
|
|
527
|
-
if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* One attempt's tool surface for a native run. `builtinHandler(name)` is the
|
|
673
|
+
* single source of truth for which names are trusted built-ins (memory,
|
|
674
|
+
* repo_*, jira_*, sub-agent/parent memory, delegate_to_*) and returns undefined
|
|
675
|
+
* for everything else, which `runUserTool` runs in the WASM sandbox. Per-attempt
|
|
676
|
+
* state (the delegation gate, siblings, issue creation counters) lives here.
|
|
677
|
+
*/
|
|
678
|
+
export function createNativeRunTools(options) {
|
|
679
|
+
const { runId, existingRun, loaded, providers, db, reviewHosts, repoAccess, issueTrackers } = options;
|
|
680
|
+
const toolsByName = new Map(Object.entries(loaded.toolsByName));
|
|
681
|
+
const secretsAccessor = buildSecretsAccessor(loaded.agentId, providers.secrets, db);
|
|
682
|
+
const sharedDatastoreAccessor = buildSharedDatastoreAccessor(loaded.agentId, providers.datastore, db);
|
|
683
|
+
// jira_create_issue's per-run cap counter: shared by every tool call of this attempt (a resumed attempt
|
|
684
|
+
// starts a fresh one, floored by the run's recorded fingerprint creates).
|
|
685
|
+
const issueCreationCounters = new Map();
|
|
686
|
+
// One per attempt: admits this run's delegations one at a time (see the delegate branch).
|
|
687
|
+
const delegationGate = createSerialGate();
|
|
688
|
+
// The children this attempt's delegations have running, and the sub-agents whose delegation is
|
|
689
|
+
// waiting for one of them to free budget (parallelDelegations): a waiting delegation keeps its
|
|
690
|
+
// place in the same-child and limit checks while the gate is open to the others.
|
|
691
|
+
const delegationSiblings = createDelegationSiblings();
|
|
692
|
+
const waitingDelegations = new Set();
|
|
693
|
+
const builtinHandler = (name) => {
|
|
694
|
+
if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
|
|
695
|
+
return async (argsJson) => {
|
|
528
696
|
return handleMemoryTool(name, argsJson, loaded.agentId, providers.memory);
|
|
529
|
-
}
|
|
530
|
-
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (REVIEW_HOST_TOOL_NAMES.has(name) && loaded.repositoryLinks.length > 0 && reviewHosts && repoAccess) {
|
|
700
|
+
return async (argsJson) => {
|
|
531
701
|
const check = await db.runHostCheck.findUnique({ where: { runId } });
|
|
532
702
|
return handleReviewHostTool(name, argsJson, {
|
|
533
703
|
agentId: loaded.agentId,
|
|
534
704
|
links: loaded.repositoryLinks,
|
|
535
705
|
hosts: reviewHosts,
|
|
706
|
+
db,
|
|
536
707
|
runCheck: check && !check.completedAt
|
|
537
708
|
? {
|
|
538
709
|
provider: check.provider,
|
|
@@ -581,11 +752,13 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
581
752
|
});
|
|
582
753
|
},
|
|
583
754
|
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// `?? []`: a load step replayed from before these links were pinned has none.
|
|
758
|
+
// Each link is re-normalised too: one pinned before the allowlists existed lacks them.
|
|
759
|
+
const issueProjectLinks = (loaded.issueProjectLinks ?? []).map(toIssueProjectLink);
|
|
760
|
+
if (ISSUE_TRACKER_TOOL_NAMES.has(name) && issueProjectLinks.length > 0 && issueTrackers) {
|
|
761
|
+
return async (argsJson) => {
|
|
589
762
|
return handleIssueTrackerTool(name, argsJson, {
|
|
590
763
|
agentId: loaded.agentId,
|
|
591
764
|
links: issueProjectLinks,
|
|
@@ -605,14 +778,20 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
605
778
|
recordedCreates: (projectKey) => db.issueFingerprint.count({ where: { createdByRunId: runId, issueProvider: "jira", projectKey } }),
|
|
606
779
|
},
|
|
607
780
|
});
|
|
608
|
-
}
|
|
609
|
-
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (name === "subagent_memory_get") {
|
|
784
|
+
return async (argsJson) => {
|
|
610
785
|
return handleSubAgentMemoryGet(argsJson, loaded.agentId, db, providers.memory);
|
|
611
|
-
}
|
|
612
|
-
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (name === "parent_memory_get") {
|
|
789
|
+
return async (argsJson) => {
|
|
613
790
|
return handleParentMemoryGet(argsJson, runId, db, providers.memory);
|
|
614
|
-
}
|
|
615
|
-
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (name.startsWith(DELEGATE_TOOL_PREFIX)) {
|
|
794
|
+
return async (argsJson) => {
|
|
616
795
|
const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
|
|
617
796
|
const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
|
|
618
797
|
if (!edge) {
|
|
@@ -630,105 +809,24 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
630
809
|
// racing on the same task. Checked and the child row written under the
|
|
631
810
|
// run's delegation gate: delegations started together in one turn
|
|
632
811
|
// (parallelDelegations) are admitted one at a time.
|
|
633
|
-
const limit = loaded.maxDelegationsPerRun ?? 1;
|
|
634
812
|
const priorDispatches = await db.run.findMany({ where: { parentRunId: { in: [runId] } } });
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
message: limit === 1
|
|
653
|
-
? "This run already delegated to a sub-agent; only one delegation is allowed per run."
|
|
654
|
-
: `This run already made ${limit} delegations, the most this agent allows per run.`,
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
let parsed;
|
|
658
|
-
try {
|
|
659
|
-
parsed = JSON.parse(argsJson || "{}");
|
|
660
|
-
}
|
|
661
|
-
catch (err) {
|
|
662
|
-
return JSON.stringify({
|
|
663
|
-
error: "invalid_arguments_json",
|
|
664
|
-
message: err instanceof Error ? err.message : String(err),
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
let args;
|
|
668
|
-
try {
|
|
669
|
-
args = DelegateArgs.parse(parsed);
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
if (err instanceof z.ZodError) {
|
|
673
|
-
return JSON.stringify({
|
|
674
|
-
error: "validation_failed",
|
|
675
|
-
message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
throw err;
|
|
679
|
-
}
|
|
680
|
-
// Owners are re-read live on every delegation (resource-sharing
|
|
681
|
-
// grants spec §3.4.4, N1): the child must have the parent's current
|
|
682
|
-
// owner, or that owner must still hold execute on the child. A
|
|
683
|
-
// revoked grant or a make_owner transfer stops the very next call.
|
|
684
|
-
const [parentNow, childAgent] = await Promise.all([
|
|
685
|
-
db.agent.findUnique({ where: { id: loaded.agentId }, select: { ownerId: true } }),
|
|
686
|
-
db.agent.findUniqueOrThrow({
|
|
687
|
-
where: { id: edge.childAgentId },
|
|
688
|
-
select: {
|
|
689
|
-
id: true,
|
|
690
|
-
kind: true,
|
|
691
|
-
budgetGroupId: true,
|
|
692
|
-
budgetUsd: true,
|
|
693
|
-
ownerId: true,
|
|
694
|
-
codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
|
|
695
|
-
},
|
|
696
|
-
}),
|
|
697
|
-
]);
|
|
698
|
-
const parentOwnerId = parentNow?.ownerId ?? null;
|
|
699
|
-
const childOwnerId = childAgent.ownerId ?? null;
|
|
700
|
-
if (!parentNow ||
|
|
701
|
-
!(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
|
|
702
|
-
return JSON.stringify({
|
|
703
|
-
error: "subagent_not_authorized",
|
|
704
|
-
message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
if (parentOwnerId !== childOwnerId) {
|
|
708
|
-
// Across owners the edge carries execute and nothing more: no
|
|
709
|
-
// model-chosen memory grant, no continuation of another run's PR,
|
|
710
|
-
// and no task text the caller couldn't give the child directly --
|
|
711
|
-
// the trigger_agent rule (review I3): a coding child only with its
|
|
712
|
-
// owner's allowWebhookTaskOverride opt-in, a native child never
|
|
713
|
-
// (it runs its owner's fixed prompt; pass an empty task).
|
|
714
|
-
const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
|
|
715
|
-
? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
|
|
716
|
-
: args.continuePriorRun !== undefined
|
|
717
|
-
? "continuePriorRun is only allowed when the sub-agent has the same owner."
|
|
718
|
-
: childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
|
|
719
|
-
? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
|
|
720
|
-
: childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
|
|
721
|
-
? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
|
|
722
|
-
: null;
|
|
723
|
-
if (refusal)
|
|
724
|
-
return JSON.stringify({ error: "cross_owner_not_allowed", message: refusal });
|
|
725
|
-
}
|
|
726
|
-
if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
|
|
727
|
-
return JSON.stringify({
|
|
728
|
-
error: "continuation_requires_coding_agent",
|
|
729
|
-
message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
|
|
730
|
-
});
|
|
731
|
-
}
|
|
813
|
+
const limitRefusal = delegationLimitRefusal({
|
|
814
|
+
priorChildAgentIds: priorDispatches.map((prior) => prior.agentId),
|
|
815
|
+
waitingChildAgentIds: [...waitingDelegations],
|
|
816
|
+
childAgentId: edge.childAgentId,
|
|
817
|
+
boundName,
|
|
818
|
+
limit: loaded.maxDelegationsPerRun ?? 1,
|
|
819
|
+
});
|
|
820
|
+
if (limitRefusal)
|
|
821
|
+
return limitRefusal;
|
|
822
|
+
const parsedArgs = parseDelegateArgs(argsJson);
|
|
823
|
+
if ("refusal" in parsedArgs)
|
|
824
|
+
return parsedArgs.refusal;
|
|
825
|
+
const { args } = parsedArgs;
|
|
826
|
+
const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
|
|
827
|
+
if ("refusal" in authorized)
|
|
828
|
+
return authorized.refusal;
|
|
829
|
+
const { childAgent } = authorized;
|
|
732
830
|
// parallelDelegations: a child the budget would refuse (run_tree_exhausted or
|
|
733
831
|
// budget_group_exhausted:*) while a sibling still runs waits for a sibling to finish and
|
|
734
832
|
// free its hold, then tries again while siblings remain, up to the child's own wait bound
|
|
@@ -798,6 +896,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
798
896
|
trigger: "subagent",
|
|
799
897
|
codingTask: args.task,
|
|
800
898
|
continuesCodingRunId: args.continuePriorRun,
|
|
899
|
+
mergeOrder: args.mergeOrder,
|
|
801
900
|
parentRunId: runId,
|
|
802
901
|
// The child's result flows back into this run, which the
|
|
803
902
|
// triggerer sees, so the child is visible to them too.
|
|
@@ -873,11 +972,78 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
873
972
|
});
|
|
874
973
|
return codingChildResult(waited.run, stored?.result);
|
|
875
974
|
}
|
|
876
|
-
const taskOverride = args
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
975
|
+
const taskOverride = delegationTaskOverride(args);
|
|
976
|
+
// mergeOrder is a coding-only concept (it orders CodingRuns within a merge); a native
|
|
977
|
+
// child has no CodingRun to store it on, so it's never passed to dispatchRun/executeRun
|
|
978
|
+
// below -- only surfaced back to the model as a note, so a lead that set it on the wrong
|
|
979
|
+
// kind of sub-agent notices rather than silently losing it.
|
|
980
|
+
const mergeOrderNote = args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined;
|
|
981
|
+
if (childAgent.nativeExecutionMode === "sandbox") {
|
|
982
|
+
// A sandbox-mode child must run where its snapshot says, so it goes through
|
|
983
|
+
// dispatchRun and the Executor (RoutingExecutor sends it to the sandbox executor),
|
|
984
|
+
// exactly like a coding child — never inline in this process.
|
|
985
|
+
if (!providers.executor) {
|
|
986
|
+
return JSON.stringify({
|
|
987
|
+
error: "sandbox_dispatch_unavailable",
|
|
988
|
+
message: "This execution context has no Executor wired in, so a sandbox-mode sub-agent cannot be dispatched.",
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
const dispatched = await dispatchRun({
|
|
992
|
+
db,
|
|
993
|
+
executor: providers.executor,
|
|
994
|
+
selfDefects: { db, issueTrackers },
|
|
995
|
+
agentId: edge.childAgentId,
|
|
996
|
+
trigger: "subagent",
|
|
997
|
+
taskOverride,
|
|
998
|
+
grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
|
|
999
|
+
parentRunId: runId,
|
|
1000
|
+
triggeredById: existingRun.triggeredById,
|
|
1001
|
+
awaitExecution: true,
|
|
1002
|
+
onPersisted: (persistedRun) => {
|
|
1003
|
+
if (persistedRun.status !== "refused" && persistedRun.status !== "failed") {
|
|
1004
|
+
finishSibling = delegationSiblings.start();
|
|
1005
|
+
}
|
|
1006
|
+
release();
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
if (!dispatched) {
|
|
1010
|
+
return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
|
|
1011
|
+
}
|
|
1012
|
+
const childRunId = dispatched.run.id;
|
|
1013
|
+
const startedAt = Date.now();
|
|
1014
|
+
runnerLog.info({ runId, childRunId, boundName, kind: "native-sandbox" }, "delegation started");
|
|
1015
|
+
// Executor.start may resolve before the child is terminal, so wait on the row like a coding child.
|
|
1016
|
+
const waited = await waitForCodingChild({
|
|
1017
|
+
db,
|
|
1018
|
+
executor: providers.executor,
|
|
1019
|
+
childRunId,
|
|
1020
|
+
parentRunId: runId,
|
|
1021
|
+
boundMs: SANDBOX_CHILD_WAIT_SEC * 1000,
|
|
1022
|
+
});
|
|
1023
|
+
finishSibling();
|
|
1024
|
+
runnerLog.info({
|
|
1025
|
+
runId,
|
|
1026
|
+
childRunId,
|
|
1027
|
+
boundName,
|
|
1028
|
+
outcome: waited.kind === "terminal" ? waited.run.status : waited.kind,
|
|
1029
|
+
durationMs: Date.now() - startedAt,
|
|
1030
|
+
}, "delegation finished");
|
|
1031
|
+
if (waited.kind === "timed_out") {
|
|
1032
|
+
return JSON.stringify({
|
|
1033
|
+
error: "subagent_wait_timed_out",
|
|
1034
|
+
runId: childRunId,
|
|
1035
|
+
message: "The sandbox sub-agent did not finish in time; it was stopped and produced no result.",
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (waited.kind === "parent_cancelled") {
|
|
1039
|
+
return JSON.stringify({
|
|
1040
|
+
error: "parent_cancelled",
|
|
1041
|
+
runId: childRunId,
|
|
1042
|
+
message: "This run was cancelled while waiting for the sandbox sub-agent; the sub-agent was stopped.",
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
return nativeChildResult(waited.run, mergeOrderNote);
|
|
1046
|
+
}
|
|
881
1047
|
const childRun = await db.run.create({
|
|
882
1048
|
data: {
|
|
883
1049
|
agentId: edge.childAgentId,
|
|
@@ -886,6 +1052,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
886
1052
|
grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
|
|
887
1053
|
taskOverride,
|
|
888
1054
|
triggeredById: existingRun.triggeredById,
|
|
1055
|
+
nativeExecutionMode: childAgent.nativeExecutionMode,
|
|
889
1056
|
},
|
|
890
1057
|
});
|
|
891
1058
|
finishSibling = delegationSiblings.start();
|
|
@@ -904,87 +1071,545 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
904
1071
|
outcome: childResult.status,
|
|
905
1072
|
durationMs: Date.now() - startedAt,
|
|
906
1073
|
}, "delegation finished");
|
|
907
|
-
return
|
|
908
|
-
status: childResult.status,
|
|
909
|
-
finalText: childResult.finalText,
|
|
910
|
-
costUsd: Number(childResult.costUsd),
|
|
911
|
-
tokensIn: childResult.tokensIn,
|
|
912
|
-
tokensOut: childResult.tokensOut,
|
|
913
|
-
});
|
|
1074
|
+
return nativeChildResult(childResult, mergeOrderNote);
|
|
914
1075
|
}
|
|
915
1076
|
finally {
|
|
916
1077
|
finishSibling();
|
|
917
1078
|
release();
|
|
918
1079
|
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return undefined;
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* The privileged host for one call of the user tool `name`, scoped to that attachment's
|
|
1086
|
+
* consented capabilities. Undefined for a name that is not an attached user tool. In-process it
|
|
1087
|
+
* serves the sandbox directly; the native sandbox gateway serves a worker's bridge calls with it.
|
|
1088
|
+
*/
|
|
1089
|
+
const scopedHost = (name, tool, signal, redactSecretValues) => createPrivilegedHost({
|
|
1090
|
+
redactSecretValues,
|
|
1091
|
+
agentId: loaded.agentId,
|
|
1092
|
+
datastore: scopeDatastore(providers.datastore, tool.allowedDatastorePrefixes),
|
|
1093
|
+
sharedDatastore: scopeSharedDatastoreAccessor(sharedDatastoreAccessor, tool.allowedSharedDatastorePrefixes),
|
|
1094
|
+
secrets: scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets),
|
|
1095
|
+
allowedFetchHosts: tool.allowedHosts,
|
|
1096
|
+
fetchImpl: providers.sandboxFetch,
|
|
1097
|
+
logTag: name,
|
|
1098
|
+
signal,
|
|
1099
|
+
});
|
|
1100
|
+
const privilegedHostFor = (name, signal, redactSecretValues) => {
|
|
1101
|
+
const tool = toolsByName.get(name);
|
|
1102
|
+
return tool ? scopedHost(name, tool, signal, redactSecretValues) : undefined;
|
|
1103
|
+
};
|
|
1104
|
+
/** Every secret value the user tool `name` may use (readable or brokered), for console redaction by a stateless gateway. */
|
|
1105
|
+
const readableSecretValues = async (name) => {
|
|
1106
|
+
const tool = toolsByName.get(name);
|
|
1107
|
+
if (!tool)
|
|
1108
|
+
return [];
|
|
1109
|
+
const scoped = scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets);
|
|
1110
|
+
const values = await Promise.all(tool.allowedSecrets.map((secret) => (scoped.resolve
|
|
1111
|
+
? scoped.resolve(secret).then((e) => (e ? consoleRedactionValues(e) : []))
|
|
1112
|
+
: scoped.get(secret).then((v) => (v === undefined ? [] : [v]))).catch(() => [])));
|
|
1113
|
+
// A SigV4 secret's key and token alone, too: the same set the in-process host redacts.
|
|
1114
|
+
return values.flat().filter((value) => typeof value === "string");
|
|
1115
|
+
};
|
|
1116
|
+
const runUserTool = async (name, argsJson) => {
|
|
1117
|
+
const tool = toolsByName.get(name);
|
|
1118
|
+
if (!tool) {
|
|
1119
|
+
return JSON.stringify({
|
|
1120
|
+
error: "unknown_tool",
|
|
1121
|
+
message: `No tool named "${name}" is attached to this agent.`,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return runUserToolCall(tool, argsJson, (signal) => scopedHost(name, tool, signal));
|
|
1125
|
+
};
|
|
1126
|
+
const runSandboxTool = async (name, argsJson) => {
|
|
1127
|
+
const builtin = builtinHandler(name);
|
|
1128
|
+
return builtin ? builtin(argsJson) : runUserTool(name, argsJson);
|
|
1129
|
+
};
|
|
1130
|
+
return { toolsByName, builtinHandler, privilegedHostFor, readableSecretValues, runUserTool, runSandboxTool };
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Live progress for observers (MCP get_run, the viewer). Absolute totals, so a DBOS replay
|
|
1134
|
+
* re-writing them is harmless. Written into the run's own cost columns on purpose: the run
|
|
1135
|
+
* tree's shared budget (computeRunTreeSpend) then counts a running parent's spend so far, as it
|
|
1136
|
+
* already does for coding runs, whose proxy ledger writes their totals live. Best-effort: a
|
|
1137
|
+
* failed write only delays what observers see, and finishRun writes the final totals anyway.
|
|
1138
|
+
*/
|
|
1139
|
+
export async function recordNativeRunProgress(db, runId, progress) {
|
|
1140
|
+
try {
|
|
1141
|
+
await db.run.updateMany({
|
|
1142
|
+
where: { id: runId, status: "running" },
|
|
1143
|
+
data: {
|
|
1144
|
+
turns: progress.turns,
|
|
1145
|
+
tokensIn: progress.usage.tokensIn,
|
|
1146
|
+
tokensOut: progress.usage.tokensOut,
|
|
1147
|
+
costUsd: progress.usage.costUsd,
|
|
1148
|
+
heartbeatAt: new Date(),
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
catch (err) {
|
|
1153
|
+
runnerLog.warn({ err, runId }, "failed to record run progress");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
/** Everything after the terminal write, shared by the normal and backstop paths. */
|
|
1157
|
+
async function settleFinishedNativeRun(ctx, finished, claimed) {
|
|
1158
|
+
const { runId, db, providers, reviewHosts, repoAccess, issueTrackers } = ctx;
|
|
1159
|
+
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1160
|
+
await completeHostStatus(db, finished, reviewHosts);
|
|
1161
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
1162
|
+
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1163
|
+
if (claimed)
|
|
1164
|
+
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1165
|
+
// A tree root is the lead of a request: once it is terminal every coding run it delegated is
|
|
1166
|
+
// too, so the reviews of their pull requests that waited for it start now, after the related
|
|
1167
|
+
// sections above are current (#259). Not gated on `claimed`: release is claimed per row, and a
|
|
1168
|
+
// lead another finalizer ended (a cancellation) should not leave them to the sweep. A run that
|
|
1169
|
+
// delegated nothing has no rows: one indexed lookup. Never throws.
|
|
1170
|
+
if (finished.parentRunId === null && providers.executor && reviewHosts && repoAccess) {
|
|
1171
|
+
await startDeferredForRequest({ db, executor: providers.executor, hosts: reviewHosts, repoAccess, issueTrackers }, runId).catch((err) => runnerLog.warn({ err, runId }, "could not start the reviews waiting for this run"));
|
|
1172
|
+
}
|
|
1173
|
+
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1174
|
+
await startReviewFixAfterReview(runId, {
|
|
1175
|
+
db,
|
|
1176
|
+
executor: providers.executor,
|
|
1177
|
+
hosts: reviewHosts,
|
|
1178
|
+
repoAccess,
|
|
1179
|
+
issueTrackers,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
1183
|
+
// filed twice. Bounded and never throws.
|
|
1184
|
+
if (claimed)
|
|
1185
|
+
await fileSelfDefect(db, issueTrackers, finished);
|
|
1186
|
+
// Only the finalizer that made the row terminal notifies. Never throws.
|
|
1187
|
+
if (claimed)
|
|
1188
|
+
await emitRunFinishedEvents(db, finished);
|
|
1189
|
+
// A lead's end posts the `wardby merge order` check on each open pull request of its set,
|
|
1190
|
+
// once every delegated coding run has opened its PR (#261); last, after the release above (so
|
|
1191
|
+
// no review waits on it) and after the follow-ups and notifications (so none is delayed by its
|
|
1192
|
+
// host reads). Only for a tree root that delegated. Not gated on `claimed`: the sync is
|
|
1193
|
+
// idempotent (an unchanged check writes nothing). Never throws.
|
|
1194
|
+
await syncMergeOrderChecksAfterRun({ db, hosts: reviewHosts }, finished);
|
|
1195
|
+
return finished;
|
|
1196
|
+
}
|
|
1197
|
+
/** The terminal write for an engine result, then its usage, host/issue status, and follow-ups. */
|
|
1198
|
+
export async function finishNativeRun(ctx, model, result) {
|
|
1199
|
+
const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
|
|
1200
|
+
status: result.status,
|
|
1201
|
+
tokensIn: result.usage.tokensIn,
|
|
1202
|
+
tokensOut: result.usage.tokensOut,
|
|
1203
|
+
costUsd: result.usage.costUsd,
|
|
1204
|
+
error: result.error ?? null,
|
|
1205
|
+
finalText: result.finalText || null,
|
|
1206
|
+
turns: result.turns,
|
|
1207
|
+
finishedAt: new Date(),
|
|
1208
|
+
});
|
|
1209
|
+
// Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
|
|
1210
|
+
await recordNativeModelUsage(ctx.db, ctx.runId, model, result.usage);
|
|
1211
|
+
return settleFinishedNativeRun(ctx, finished, claimed);
|
|
1212
|
+
}
|
|
1213
|
+
/** The backstop terminal write for a run that threw. A cancellation is not a failure: it carries the operator's own reason. */
|
|
1214
|
+
export async function failNativeRun(ctx, err) {
|
|
1215
|
+
const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
|
|
1216
|
+
status: err instanceof RunCancelledError || err instanceof SandboxRunCancelledError ? "cancelled" : "failed",
|
|
1217
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1218
|
+
finishedAt: new Date(),
|
|
1219
|
+
});
|
|
1220
|
+
return settleFinishedNativeRun(ctx, finished, claimed);
|
|
1221
|
+
}
|
|
1222
|
+
/** Where a run stands for the native gateway: still drivable, cancelled (its task), or otherwise ended. */
|
|
1223
|
+
export async function runDrivability(db, runId) {
|
|
1224
|
+
const run = await db.run.findUnique({ where: { id: runId }, select: { status: true } });
|
|
1225
|
+
if (!run || !DRIVABLE.includes(run.status)) {
|
|
1226
|
+
return run?.status === "cancelled" ? "cancelled" : "ended";
|
|
1227
|
+
}
|
|
1228
|
+
const cancelledTask = await db.task.findFirst({ where: { runId, status: "cancelled" }, select: { id: true } });
|
|
1229
|
+
return cancelledTask ? "cancelled" : "drivable";
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* What a sandbox worker is given: the pinned agent and tool definitions, which names are built-ins
|
|
1233
|
+
* (served by the gateway), and each user tool's code and schema only — never its capabilities,
|
|
1234
|
+
* secret names, or values, which stay with the gateway's privileged host.
|
|
1235
|
+
*/
|
|
1236
|
+
export function sandboxWorkerInput(runId, loaded, tools) {
|
|
1237
|
+
if (!loaded.pricing) {
|
|
1238
|
+
throw new Error("native_sandbox_requires_catalog: a sandbox run needs its model's catalog entry, and none was recorded.");
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
v: NATIVE_WORKER_PROTOCOL_VERSION,
|
|
1242
|
+
runId,
|
|
1243
|
+
agent: loaded.agent,
|
|
1244
|
+
tools: loaded.tools,
|
|
1245
|
+
builtinTools: loaded.tools.map((tool) => tool.name).filter((name) => tools.builtinHandler(name) !== undefined),
|
|
1246
|
+
userTools: Object.fromEntries([...tools.toolsByName].map(([name, tool]) => [name, { code: tool.code, paramsZod: tool.paramsZod }])),
|
|
1247
|
+
runsConcurrently: loaded.parallelDelegations
|
|
1248
|
+
? loaded.tools.map((tool) => tool.name).filter((name) => name.startsWith(DELEGATE_TOOL_PREFIX))
|
|
1249
|
+
: [],
|
|
1250
|
+
pricing: { ...loaded.pricing.entry, efforts: [...loaded.pricing.entry.efforts] },
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
/** Long-poll window: well under common proxy and load-balancer idle timeouts. */
|
|
1254
|
+
export const DELEGATION_POLL_WINDOW_MS = 20_000;
|
|
1255
|
+
/**
|
|
1256
|
+
* A delegate_to_* call from a sandboxed run, served by the native gateway. The in-process branch
|
|
1257
|
+
* holds its admission, budget wait, and child wait in one process's memory for the child's whole
|
|
1258
|
+
* life; this one keeps them on the call's ledger row, so the worker can call again with the same
|
|
1259
|
+
* callId — on any control-plane replica, before or after a restart — and continue where it was:
|
|
1260
|
+
*
|
|
1261
|
+
* - every child, native or coding, is a managed run dispatched through dispatchRun and the
|
|
1262
|
+
* Executor (never inline: an inline child dies with the replica, and the reconciler does not
|
|
1263
|
+
* reap unmanaged runs);
|
|
1264
|
+
* - admission re-checks the per-run delegation limit under a run-scoped advisory lock inside the
|
|
1265
|
+
* child's own transaction, so parallel delegations on different replicas cannot exceed it;
|
|
1266
|
+
* - a delegation the budget would refuse while sibling children still run is recorded as waiting
|
|
1267
|
+
* and re-evaluated on each call (parallelDelegations), up to the same bound as in process;
|
|
1268
|
+
* - each call waits on the child row up to `pollWindowMs`, then answers `pending`.
|
|
1269
|
+
*
|
|
1270
|
+
* Every refusal and result uses the same helpers, and so the same messages, as in process.
|
|
1271
|
+
*/
|
|
1272
|
+
export async function durableDelegate(ctx, callId, name, argsJson) {
|
|
1273
|
+
const { runId, existingRun, loaded, providers, db, ledger, sessionId } = ctx;
|
|
1274
|
+
const now = ctx.now ?? Date.now;
|
|
1275
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1276
|
+
const finish = async (result) => {
|
|
1277
|
+
await ledger.recordResult(sessionId, callId, result);
|
|
1278
|
+
return { result };
|
|
1279
|
+
};
|
|
1280
|
+
const claimed = await ledger.claim(sessionId, callId, "builtin.call");
|
|
1281
|
+
if (claimed.outcome === "done")
|
|
1282
|
+
return { result: String(claimed.result) };
|
|
1283
|
+
const state = await ledger.delegation(sessionId, callId);
|
|
1284
|
+
const startedAt = state?.createdAt.getTime() ?? now();
|
|
1285
|
+
const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
|
|
1286
|
+
const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
|
|
1287
|
+
if (!edge) {
|
|
1288
|
+
return finish(JSON.stringify({ error: "no_such_subagent", message: `No sub-agent is bound to name "${boundName}".` }));
|
|
1289
|
+
}
|
|
1290
|
+
const executor = providers.executor;
|
|
1291
|
+
if (!executor) {
|
|
1292
|
+
return finish(JSON.stringify({
|
|
1293
|
+
error: "delegation_unavailable",
|
|
1294
|
+
message: "This execution context has no Executor wired in, so the sub-agent cannot be dispatched.",
|
|
1295
|
+
}));
|
|
1296
|
+
}
|
|
1297
|
+
const parsedArgs = parseDelegateArgs(argsJson);
|
|
1298
|
+
if ("refusal" in parsedArgs)
|
|
1299
|
+
return finish(parsedArgs.refusal);
|
|
1300
|
+
const { args } = parsedArgs;
|
|
1301
|
+
const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
|
|
1302
|
+
if ("refusal" in authorized)
|
|
1303
|
+
return finish(authorized.refusal);
|
|
1304
|
+
const { childAgent } = authorized;
|
|
1305
|
+
const coding = childAgent.kind === "coding";
|
|
1306
|
+
const { queueTimeoutSec } = loadCodingConcurrencyConfig();
|
|
1307
|
+
let childRunId = state?.childRunId ?? null;
|
|
1308
|
+
if (!childRunId && state?.childAgentId === edge.childAgentId) {
|
|
1309
|
+
// A replica that died between dispatching the child and recording it left the child row:
|
|
1310
|
+
// adopt it rather than refusing this very delegation as a duplicate.
|
|
1311
|
+
const orphan = await db.run.findFirst({
|
|
1312
|
+
where: { parentRunId: runId, agentId: edge.childAgentId },
|
|
1313
|
+
select: { id: true },
|
|
1314
|
+
});
|
|
1315
|
+
childRunId = orphan?.id ?? null;
|
|
1316
|
+
}
|
|
1317
|
+
if (!childRunId) {
|
|
1318
|
+
const limit = loaded.maxDelegationsPerRun ?? 1;
|
|
1319
|
+
const refusalNow = async (reader) => {
|
|
1320
|
+
const prior = await reader.run.findMany({ where: { parentRunId: runId }, select: { agentId: true } });
|
|
1321
|
+
return delegationLimitRefusal({
|
|
1322
|
+
priorChildAgentIds: prior.map((p) => p.agentId),
|
|
1323
|
+
waitingChildAgentIds: await ledger.waitingChildAgentIds(sessionId, callId),
|
|
1324
|
+
childAgentId: edge.childAgentId,
|
|
1325
|
+
boundName,
|
|
1326
|
+
limit,
|
|
952
1327
|
});
|
|
953
|
-
if (!result.ok) {
|
|
954
|
-
return JSON.stringify({ error: result.errorKind, message: result.errorMessage });
|
|
955
|
-
}
|
|
956
|
-
return JSON.stringify(result.value);
|
|
957
1328
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
});
|
|
1329
|
+
const limitRefusal = await refusalNow(db);
|
|
1330
|
+
if (limitRefusal)
|
|
1331
|
+
return finish(limitRefusal);
|
|
1332
|
+
// parallelDelegations: a child the budget would refuse while a sibling still runs waits for a
|
|
1333
|
+
// sibling to finish and free its hold, up to the child's own wait bound. With no sibling
|
|
1334
|
+
// running it goes ahead and is refused at dispatch or at its first turn, as in process.
|
|
1335
|
+
if (loaded.parallelDelegations) {
|
|
1336
|
+
const siblings = await db.run.count({ where: { parentRunId: runId, status: { in: [...DRIVABLE] } } });
|
|
1337
|
+
if (siblings > 0) {
|
|
1338
|
+
const runTimeoutSec = coding ? (childAgent.codingProfile?.timeoutSec ?? 0) : 0;
|
|
1339
|
+
const boundMs = (queueTimeoutSec + runTimeoutSec + CODING_CHILD_WAIT_GRACE_SEC) * 1000;
|
|
1340
|
+
const { exhaustedBy } = await effectiveBudgetForRun(db, childAgent, new Date(), runId);
|
|
1341
|
+
if (exhaustedBy && now() - startedAt < boundMs) {
|
|
1342
|
+
await ledger.setDelegation(sessionId, callId, { status: "waiting_budget", childAgentId: edge.childAgentId });
|
|
1343
|
+
return { pending: true };
|
|
1344
|
+
}
|
|
975
1345
|
}
|
|
976
|
-
|
|
977
|
-
|
|
1346
|
+
}
|
|
1347
|
+
// Recorded before dispatch, so a replica dying in between leaves a row the next call adopts.
|
|
1348
|
+
await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId });
|
|
1349
|
+
let dispatched;
|
|
1350
|
+
let refusedUnderLock = null;
|
|
1351
|
+
try {
|
|
1352
|
+
dispatched = await dispatchRun({
|
|
1353
|
+
db,
|
|
1354
|
+
executor,
|
|
1355
|
+
selfDefects: { db, issueTrackers: ctx.issueTrackers },
|
|
1356
|
+
agentId: edge.childAgentId,
|
|
1357
|
+
trigger: "subagent",
|
|
1358
|
+
...(coding
|
|
1359
|
+
? { codingTask: args.task, continuesCodingRunId: args.continuePriorRun, mergeOrder: args.mergeOrder }
|
|
1360
|
+
: { taskOverride: delegationTaskOverride(args), grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [] }),
|
|
1361
|
+
parentRunId: runId,
|
|
1362
|
+
// The child's result flows back into this run, which the triggerer sees, so the child is visible to them too.
|
|
1363
|
+
triggeredById: existingRun.triggeredById,
|
|
1364
|
+
beforePersist: async (tx) => {
|
|
1365
|
+
// One admission per parent at a time, across every replica.
|
|
1366
|
+
await tx.$queryRaw `SELECT 1 AS locked FROM (SELECT pg_advisory_xact_lock(hashtext(${runId}))) AS l`;
|
|
1367
|
+
refusedUnderLock = await refusalNow(tx);
|
|
1368
|
+
return refusedUnderLock === null;
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
catch (err) {
|
|
1373
|
+
if (!(err instanceof ContinuationRefusedError))
|
|
1374
|
+
throw err;
|
|
1375
|
+
return finish(JSON.stringify({
|
|
1376
|
+
error: "continuation_refused",
|
|
1377
|
+
message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
|
|
1378
|
+
"tell the requester that this wardby deployment cannot continue that pull request's branch.",
|
|
1379
|
+
}));
|
|
1380
|
+
}
|
|
1381
|
+
if (!dispatched) {
|
|
1382
|
+
return finish(refusedUnderLock ??
|
|
1383
|
+
JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." }));
|
|
1384
|
+
}
|
|
1385
|
+
childRunId = dispatched.run.id;
|
|
1386
|
+
await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId, childRunId });
|
|
1387
|
+
runnerLog.info({ runId, childRunId, boundName, kind: childAgent.kind, durable: true }, "delegation started");
|
|
1388
|
+
}
|
|
1389
|
+
const codingRun = coding
|
|
1390
|
+
? await db.codingRun.findUnique({ where: { runId: childRunId }, select: { timeoutSec: true } })
|
|
1391
|
+
: null;
|
|
1392
|
+
const boundMs = coding
|
|
1393
|
+
? (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000
|
|
1394
|
+
: SANDBOX_CHILD_WAIT_SEC * 1000;
|
|
1395
|
+
const windowEnd = now() + (ctx.pollWindowMs ?? DELEGATION_POLL_WINDOW_MS);
|
|
1396
|
+
for (;;) {
|
|
1397
|
+
const child = await db.run.findUniqueOrThrow({ where: { id: childRunId } });
|
|
1398
|
+
if (!DRIVABLE.includes(child.status)) {
|
|
1399
|
+
runnerLog.info({ runId, childRunId, boundName, outcome: child.status, durable: true }, "delegation finished");
|
|
1400
|
+
if (!coding) {
|
|
1401
|
+
return finish(nativeChildResult(child, args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined));
|
|
978
1402
|
}
|
|
979
|
-
|
|
1403
|
+
const stored = await db.codingRun
|
|
1404
|
+
.findUnique({ where: { runId: childRunId }, select: { result: true } })
|
|
1405
|
+
.catch(() => null);
|
|
1406
|
+
return finish(codingChildResult(child, stored?.result));
|
|
1407
|
+
}
|
|
1408
|
+
if (now() - startedAt >= boundMs) {
|
|
1409
|
+
await executor.stop(childRunId, "sub-agent wait timed out").catch((err) => {
|
|
1410
|
+
runnerLog.warn({ err, childRunId, runId }, "failed to stop a timed-out sub-agent run");
|
|
1411
|
+
});
|
|
1412
|
+
return finish(JSON.stringify({
|
|
1413
|
+
error: "subagent_wait_timed_out",
|
|
1414
|
+
runId: childRunId,
|
|
1415
|
+
message: coding
|
|
1416
|
+
? "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result."
|
|
1417
|
+
: "The sub-agent did not finish in time; it was stopped and produced no result.",
|
|
1418
|
+
}));
|
|
1419
|
+
}
|
|
1420
|
+
const remaining = windowEnd - now();
|
|
1421
|
+
if (remaining <= 0)
|
|
1422
|
+
return { pending: true };
|
|
1423
|
+
await sleep(Math.min(ctx.pollIntervalMs ?? 1_000, remaining));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/** The integrations a native run uses, as executeRun derives them from its providers. */
|
|
1427
|
+
export function nativeRunIntegrations(providers, db) {
|
|
1428
|
+
const reviewHosts = configuredReviewHosts(providers.reviewHosts);
|
|
1429
|
+
const repoAccess = reviewHosts
|
|
1430
|
+
? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
|
|
1431
|
+
: undefined;
|
|
1432
|
+
return { reviewHosts, repoAccess, issueTrackers: configuredIssueTrackers(providers.issueTrackers) };
|
|
1433
|
+
}
|
|
1434
|
+
/** How long a sandbox run's gateway session (and so its capability) stays valid. */
|
|
1435
|
+
export const SANDBOX_RUN_MAX_SEC = SANDBOX_CHILD_WAIT_SEC;
|
|
1436
|
+
/**
|
|
1437
|
+
* Starts a sandbox-mode run's trusted half: loads and pins the run, marks it running, records its
|
|
1438
|
+
* gateway session (capability hash, deadline, budget, snapshot), and records `native-sandbox` as
|
|
1439
|
+
* its execution backend — all before any worker exists. The caller launches the worker with the
|
|
1440
|
+
* returned input, which carries the gateway URL and the one-time capability.
|
|
1441
|
+
*/
|
|
1442
|
+
export async function createSandboxSession(options) {
|
|
1443
|
+
const { runId, providers, db, ledger } = options;
|
|
1444
|
+
const now = options.now ?? new Date();
|
|
1445
|
+
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
1446
|
+
if (!existingRun)
|
|
1447
|
+
throw new Error(`Unknown run "${runId}".`);
|
|
1448
|
+
if (!DRIVABLE.includes(existingRun.status))
|
|
1449
|
+
return { kind: "ended", run: existingRun };
|
|
1450
|
+
if (existingRun.nativeExecutionMode !== "sandbox")
|
|
1451
|
+
throw new Error(`Run "${runId}" is not a sandbox-mode run.`);
|
|
1452
|
+
const { reviewHosts, repoAccess, issueTrackers } = nativeRunIntegrations(providers, db);
|
|
1453
|
+
const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
|
|
1454
|
+
const loadedOrUnavailable = await loadNativeRun({
|
|
1455
|
+
runId,
|
|
1456
|
+
existingRun,
|
|
1457
|
+
providers,
|
|
1458
|
+
db,
|
|
1459
|
+
step: runStepInline,
|
|
1460
|
+
reviewHosts,
|
|
1461
|
+
issueTrackers,
|
|
1462
|
+
});
|
|
1463
|
+
if ("unavailable" in loadedOrUnavailable) {
|
|
1464
|
+
return { kind: "ended", run: await failNativeRun(finishContext, new Error(loadedOrUnavailable.unavailable)) };
|
|
1465
|
+
}
|
|
1466
|
+
const loaded = loadedOrUnavailable;
|
|
1467
|
+
if (loaded.kind === "coding") {
|
|
1468
|
+
return { kind: "ended", run: await failNativeRun(finishContext, new Error(CODING_EXECUTOR_NOT_CONFIGURED)) };
|
|
1469
|
+
}
|
|
1470
|
+
const tools = createNativeRunTools({
|
|
1471
|
+
runId,
|
|
1472
|
+
existingRun,
|
|
1473
|
+
loaded,
|
|
1474
|
+
providers,
|
|
1475
|
+
db,
|
|
1476
|
+
reviewHosts,
|
|
1477
|
+
repoAccess,
|
|
1478
|
+
issueTrackers,
|
|
1479
|
+
});
|
|
1480
|
+
let input;
|
|
1481
|
+
try {
|
|
1482
|
+
input = sandboxWorkerInput(runId, loaded, tools);
|
|
1483
|
+
}
|
|
1484
|
+
catch (err) {
|
|
1485
|
+
return { kind: "ended", run: await failNativeRun(finishContext, err) };
|
|
1486
|
+
}
|
|
1487
|
+
await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
|
|
1488
|
+
const capability = randomBytes(32).toString("base64url");
|
|
1489
|
+
const snapshot = { input, loaded };
|
|
1490
|
+
const session = await ledger.createSession({
|
|
1491
|
+
runId,
|
|
1492
|
+
capabilityHash: sandboxCapabilityHash(capability),
|
|
1493
|
+
deadlineAt: new Date(now.getTime() + SANDBOX_RUN_MAX_SEC * 1000),
|
|
1494
|
+
budgetUsd: loaded.agent.budgetUsd,
|
|
1495
|
+
snapshot: snapshot,
|
|
1496
|
+
networkReadyAt: options.networkReady === false ? null : now,
|
|
1497
|
+
});
|
|
1498
|
+
// The handle exists before any worker does, so the reconciler routes a stale run to the sandbox executor.
|
|
1499
|
+
await db.run.updateMany({
|
|
1500
|
+
where: { id: runId, executionBackend: null },
|
|
1501
|
+
data: { executionBackend: NATIVE_SANDBOX_BACKEND },
|
|
1502
|
+
});
|
|
1503
|
+
return {
|
|
1504
|
+
kind: "started",
|
|
1505
|
+
sessionId: session.id,
|
|
1506
|
+
capability,
|
|
1507
|
+
input: { ...input, gateway: { url: options.gatewayUrl, capability } },
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
/** Run.executionBackend for a sandbox-mode run whose worker is (or was) launched. */
|
|
1511
|
+
export const NATIVE_SANDBOX_BACKEND = "native-sandbox";
|
|
1512
|
+
/** The stored form of a sandbox capability: never the token itself. */
|
|
1513
|
+
export function sandboxCapabilityHash(capability) {
|
|
1514
|
+
return createHash("sha256").update(capability).digest("hex");
|
|
1515
|
+
}
|
|
1516
|
+
/** Drives an existing Run (created by `createRun` or the scheduler) to a terminal state. */
|
|
1517
|
+
export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
|
|
1518
|
+
// Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
|
|
1519
|
+
return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
|
|
1520
|
+
}
|
|
1521
|
+
async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
1522
|
+
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
1523
|
+
if (!existingRun) {
|
|
1524
|
+
throw new Error(`Unknown run "${runId}".`);
|
|
1525
|
+
}
|
|
1526
|
+
// Pre-flight guard. A run that already reached a terminal state must never
|
|
1527
|
+
// be re-driven: a durable workflow re-dispatched after the reconciler
|
|
1528
|
+
// reaped its row (rollback to EXECUTOR=in-process, then roll forward) would
|
|
1529
|
+
// otherwise re-spend the whole run against a `lost` row, and a duplicate
|
|
1530
|
+
// attempt of a finished run would spend a second time for a result no write
|
|
1531
|
+
// can land. Cheaper and clearer than letting it run and discarding the
|
|
1532
|
+
// result at the conditional write.
|
|
1533
|
+
if (!DRIVABLE.includes(existingRun.status)) {
|
|
1534
|
+
runnerLog.info({ runId, status: existingRun.status }, "skipping execution of an already-terminal run");
|
|
1535
|
+
return existingRun;
|
|
1536
|
+
}
|
|
1537
|
+
const reviewHosts = configuredReviewHosts(providers.reviewHosts);
|
|
1538
|
+
const repoAccess = reviewHosts
|
|
1539
|
+
? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
|
|
1540
|
+
: undefined;
|
|
1541
|
+
const issueTrackers = configuredIssueTrackers(providers.issueTrackers);
|
|
1542
|
+
// Fails a run that ends before its engine starts, closing what its trigger opened (the review
|
|
1543
|
+
// check, the host and issue status comments) exactly as the normal and catch paths below do.
|
|
1544
|
+
// Deliberately no self-defect: every caller is a configuration state, not this run's defect (an
|
|
1545
|
+
// admin disabled or removed the model, or this deployment has no coding or native sandbox
|
|
1546
|
+
// executor), and filing would open one defect per affected agent rather than describe a failure
|
|
1547
|
+
// of that agent.
|
|
1548
|
+
const finishEarly = async (error) => {
|
|
1549
|
+
const finished = await finishRun(db, runId, { status: "failed", error, finishedAt: new Date() });
|
|
1550
|
+
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1551
|
+
await completeHostStatus(db, finished, reviewHosts);
|
|
1552
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
1553
|
+
await emitRunFinishedEvents(db, finished);
|
|
1554
|
+
return finished;
|
|
1555
|
+
};
|
|
1556
|
+
// Decided by the run's own snapshot, before any work or spend (no pricing pin, no engine): a
|
|
1557
|
+
// sandbox-mode run never executes in the control plane.
|
|
1558
|
+
const sandboxed = existingRun.nativeExecutionMode === "sandbox";
|
|
1559
|
+
if (sandboxed && !providers.nativeSandbox)
|
|
1560
|
+
return finishEarly(NATIVE_SANDBOX_NOT_CONFIGURED);
|
|
1561
|
+
const loadedOrUnavailable = await loadNativeRun({
|
|
1562
|
+
runId,
|
|
1563
|
+
existingRun,
|
|
1564
|
+
providers,
|
|
1565
|
+
db,
|
|
1566
|
+
step,
|
|
1567
|
+
reviewHosts,
|
|
1568
|
+
issueTrackers,
|
|
1569
|
+
});
|
|
1570
|
+
if ("unavailable" in loadedOrUnavailable)
|
|
1571
|
+
return finishEarly(loadedOrUnavailable.unavailable);
|
|
1572
|
+
const loaded = loadedOrUnavailable;
|
|
1573
|
+
if (loaded.kind === "coding")
|
|
1574
|
+
return finishEarly(CODING_EXECUTOR_NOT_CONFIGURED);
|
|
1575
|
+
// Conditional on DRIVABLE rather than on `pending`: an adopted attempt
|
|
1576
|
+
// legitimately finds the row already `running`, but a terminal row must
|
|
1577
|
+
// never be flipped back to `running`.
|
|
1578
|
+
await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
|
|
1579
|
+
const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
|
|
1580
|
+
try {
|
|
1581
|
+
const tools = createNativeRunTools({
|
|
1582
|
+
runId,
|
|
1583
|
+
existingRun,
|
|
1584
|
+
loaded,
|
|
1585
|
+
providers,
|
|
1586
|
+
db,
|
|
1587
|
+
reviewHosts,
|
|
1588
|
+
repoAccess,
|
|
1589
|
+
issueTrackers,
|
|
1590
|
+
});
|
|
1591
|
+
const { runSandboxTool } = tools;
|
|
1592
|
+
const onProgress = (progress) => recordNativeRunProgress(db, runId, progress);
|
|
1593
|
+
const llm = loaded.pricing && providers.llm instanceof RoutingLlmProvider
|
|
1594
|
+
? providers.llm.forRun(loaded.pricing.entry)
|
|
1595
|
+
: providers.llm;
|
|
1596
|
+
if (sandboxed && providers.nativeSandbox) {
|
|
1597
|
+
// The turn loop runs in a worker; this process serves its calls from the same tools and LLM.
|
|
1598
|
+
const engineResult = await runSandboxedEngine({
|
|
1599
|
+
runId,
|
|
1600
|
+
input: sandboxWorkerInput(runId, loaded, tools),
|
|
1601
|
+
ctx: { providers: { llm }, onText, onProgress },
|
|
1602
|
+
builtinHandler: tools.builtinHandler,
|
|
1603
|
+
privilegedHostFor: tools.privilegedHostFor,
|
|
1604
|
+
drivability: () => runDrivability(db, runId),
|
|
1605
|
+
launcher: providers.nativeSandbox,
|
|
1606
|
+
});
|
|
1607
|
+
return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
|
|
1608
|
+
}
|
|
980
1609
|
const engineResult = await providers.engine.run({
|
|
981
1610
|
agent: loaded.agent,
|
|
982
1611
|
tools: loaded.tools,
|
|
983
|
-
providers: {
|
|
984
|
-
llm: loaded.pricing && providers.llm instanceof RoutingLlmProvider
|
|
985
|
-
? providers.llm.forRun(loaded.pricing.entry)
|
|
986
|
-
: providers.llm,
|
|
987
|
-
},
|
|
1612
|
+
providers: { llm },
|
|
988
1613
|
runSandboxTool,
|
|
989
1614
|
// A replay recorded before this flag existed has none: its delegations stay sequential.
|
|
990
1615
|
...(loaded.parallelDelegations
|
|
@@ -994,70 +1619,22 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
994
1619
|
onProgress,
|
|
995
1620
|
step,
|
|
996
1621
|
});
|
|
997
|
-
|
|
998
|
-
status: engineResult.status,
|
|
999
|
-
tokensIn: engineResult.usage.tokensIn,
|
|
1000
|
-
tokensOut: engineResult.usage.tokensOut,
|
|
1001
|
-
costUsd: engineResult.usage.costUsd,
|
|
1002
|
-
error: engineResult.error ?? null,
|
|
1003
|
-
finalText: engineResult.finalText || null,
|
|
1004
|
-
turns: engineResult.turns,
|
|
1005
|
-
finishedAt: new Date(),
|
|
1006
|
-
});
|
|
1007
|
-
// Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
|
|
1008
|
-
await recordNativeModelUsage(db, runId, loaded.agent.model, engineResult.usage);
|
|
1009
|
-
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1010
|
-
await completeHostStatus(db, finished, reviewHosts);
|
|
1011
|
-
await completeIssueStatus(db, finished, issueTrackers);
|
|
1012
|
-
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1013
|
-
if (claimed)
|
|
1014
|
-
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1015
|
-
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1016
|
-
await startReviewFixAfterReview(runId, {
|
|
1017
|
-
db,
|
|
1018
|
-
executor: providers.executor,
|
|
1019
|
-
hosts: reviewHosts,
|
|
1020
|
-
repoAccess,
|
|
1021
|
-
issueTrackers,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
1025
|
-
// filed twice. Bounded and never throws.
|
|
1026
|
-
if (claimed)
|
|
1027
|
-
await fileSelfDefect(db, issueTrackers, finished);
|
|
1028
|
-
return finished;
|
|
1622
|
+
return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
|
|
1029
1623
|
}
|
|
1030
1624
|
catch (err) {
|
|
1031
1625
|
// Defensive backstop: the engine is expected to catch its own errors
|
|
1032
1626
|
// and return a "failed" EngineResult, but an unexpected throw here
|
|
1033
1627
|
// (a real bug, or tool-loading failing outside the per-tool try above)
|
|
1034
|
-
// must still never leave the run dangling in "running".
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
await completeHostStatus(db, finished, reviewHosts);
|
|
1043
|
-
await completeIssueStatus(db, finished, issueTrackers);
|
|
1044
|
-
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1045
|
-
if (claimed)
|
|
1046
|
-
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1047
|
-
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1048
|
-
await startReviewFixAfterReview(runId, {
|
|
1049
|
-
db,
|
|
1050
|
-
executor: providers.executor,
|
|
1051
|
-
hosts: reviewHosts,
|
|
1052
|
-
repoAccess,
|
|
1053
|
-
issueTrackers,
|
|
1054
|
-
});
|
|
1628
|
+
// must still never leave the run dangling in "running".
|
|
1629
|
+
if (err instanceof RunOwnershipLostError) {
|
|
1630
|
+
// Another execution of this run recorded a step first and owns the run now. Write nothing
|
|
1631
|
+
// and fire no settle side effects: the winner makes the terminal write. Rethrown (not
|
|
1632
|
+
// returned) so the durable executor hands the conflict back to its SDK, which parks this
|
|
1633
|
+
// attempt instead of recording a workflow outcome under the winner.
|
|
1634
|
+
logRunOwnershipLost(runId, err);
|
|
1635
|
+
throw err;
|
|
1055
1636
|
}
|
|
1056
|
-
|
|
1057
|
-
// filed twice. Bounded and never throws.
|
|
1058
|
-
if (claimed)
|
|
1059
|
-
await fileSelfDefect(db, issueTrackers, finished);
|
|
1060
|
-
return finished;
|
|
1637
|
+
return failNativeRun(finishContext, err);
|
|
1061
1638
|
}
|
|
1062
1639
|
}
|
|
1063
1640
|
/**
|