@wardby/cli 0.3.0 → 0.4.1
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 +22 -3
- package/README.md +11 -9
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli.js +68 -10
- package/dist/coding/base-commit.d.ts +6 -0
- package/dist/coding/base-commit.js +12 -0
- package/dist/coding/protocol.d.ts +17 -1
- package/dist/coding/protocol.js +17 -6
- package/dist/coding/provider.d.ts +6 -1
- package/dist/coding/provider.js +16 -9
- package/dist/config/providers.d.ts +25 -0
- package/dist/config/providers.js +71 -0
- package/dist/core/attribution.d.ts +101 -0
- package/dist/core/attribution.js +208 -0
- package/dist/core/budget-groups.d.ts +17 -10
- package/dist/core/budget-groups.js +15 -12
- package/dist/core/coding-queue.d.ts +3 -0
- package/dist/core/coding-queue.js +6 -2
- package/dist/core/coding-service-status.d.ts +11 -0
- package/dist/core/coding-service-status.js +17 -0
- package/dist/core/cost-report.d.ts +88 -0
- package/dist/core/cost-report.js +248 -0
- package/dist/core/dispatch.d.ts +42 -2
- package/dist/core/dispatch.js +144 -26
- package/dist/core/engine-native.js +17 -4
- package/dist/core/glob.d.ts +10 -0
- package/dist/core/glob.js +33 -0
- package/dist/core/host-events.d.ts +24 -1
- package/dist/core/host-events.js +341 -1
- package/dist/core/host-status.d.ts +19 -2
- package/dist/core/host-status.js +47 -30
- package/dist/core/issue-bridge.d.ts +60 -0
- package/dist/core/issue-bridge.js +189 -0
- package/dist/core/issue-dedupe.d.ts +70 -0
- package/dist/core/issue-dedupe.js +255 -0
- package/dist/core/issue-events.d.ts +42 -0
- package/dist/core/issue-events.js +155 -0
- package/dist/core/issue-status.d.ts +29 -0
- package/dist/core/issue-status.js +241 -0
- package/dist/core/issue-tracker-tools.d.ts +64 -0
- package/dist/core/issue-tracker-tools.js +850 -0
- package/dist/core/model-usage.d.ts +10 -0
- package/dist/core/model-usage.js +24 -0
- package/dist/core/reconciler.d.ts +8 -4
- package/dist/core/reconciler.js +15 -4
- package/dist/core/review-host-tools.js +10 -3
- package/dist/core/run-pricing.d.ts +61 -0
- package/dist/core/run-pricing.js +56 -0
- package/dist/core/runner.d.ts +5 -2
- package/dist/core/runner.js +178 -27
- package/dist/core/scheduler.d.ts +4 -1
- package/dist/core/scheduler.js +3 -2
- package/dist/core/self-defects.d.ts +80 -0
- package/dist/core/self-defects.js +180 -0
- package/dist/core/tool-names.js +3 -0
- package/dist/core/webhooks.d.ts +9 -1
- package/dist/core/webhooks.js +19 -1
- package/dist/env.js +6 -1
- package/dist/generated/prisma/browser.d.ts +66 -0
- package/dist/generated/prisma/client.d.ts +66 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
- package/dist/generated/prisma/enums.d.ts +7 -0
- package/dist/generated/prisma/enums.js +6 -0
- package/dist/generated/prisma/internal/class.d.ts +99 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
- package/dist/generated/prisma/models/Agent.d.ts +389 -1
- package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
- package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
- package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
- package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
- package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
- package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
- package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
- package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +933 -1
- package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
- package/dist/generated/prisma/models/RunAttribution.js +1 -0
- package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
- package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
- package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
- package/dist/generated/prisma/models/RunModelUsage.js +1 -0
- package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
- package/dist/generated/prisma/models/WorkItem.js +1 -0
- package/dist/generated/prisma/models.d.ts +9 -0
- package/dist/help-index.json +355 -16
- package/dist/import/neutral-schema.d.ts +16 -16
- package/dist/knowledge/check.d.ts +13 -0
- package/dist/knowledge/check.js +69 -0
- package/dist/knowledge/cli.d.ts +14 -0
- package/dist/knowledge/cli.js +67 -0
- package/dist/knowledge/concept.d.ts +54 -0
- package/dist/knowledge/concept.js +78 -0
- package/dist/knowledge/note.d.ts +11 -0
- package/dist/knowledge/note.js +39 -0
- package/dist/knowledge/relevance.d.ts +11 -0
- package/dist/knowledge/relevance.js +14 -0
- package/dist/knowledge/span-hash.d.ts +3 -0
- package/dist/knowledge/span-hash.js +16 -0
- package/dist/mcp/auth/access.d.ts +4 -2
- package/dist/mcp/auth/ownership.d.ts +9 -9
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +18 -3
- package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/host-events/deliveries.d.ts +9 -0
- package/dist/mcp/host-events/deliveries.js +17 -0
- package/dist/mcp/host-events/github-ingress.d.ts +4 -2
- package/dist/mcp/host-events/github-ingress.js +4 -13
- package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
- package/dist/mcp/host-events/jira-ingress.js +92 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +87 -9
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/tools/agents.js +74 -3
- package/dist/mcp/tools/cost-report.d.ts +8 -0
- package/dist/mcp/tools/cost-report.js +60 -0
- package/dist/mcp/tools/issue-projects.d.ts +2 -0
- package/dist/mcp/tools/issue-projects.js +238 -0
- package/dist/mcp/tools/model-catalog.d.ts +22 -0
- package/dist/mcp/tools/model-catalog.js +423 -0
- package/dist/mcp/tools/repositories.js +2 -1
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/trigger.js +33 -5
- package/dist/mcp/transport/streamable-http.d.ts +5 -0
- package/dist/mcp/transport/streamable-http.js +23 -1
- package/dist/mcp/webhooks/ingress.d.ts +2 -1
- package/dist/mcp/webhooks/ingress.js +9 -2
- package/dist/providers/auth/self-hosted.d.ts +8 -1
- package/dist/providers/auth/self-hosted.js +39 -2
- package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
- package/dist/providers/coding-proxy/memory-ledger.js +10 -1
- package/dist/providers/coding-proxy/metering.d.ts +2 -1
- package/dist/providers/coding-proxy/metering.js +13 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
- package/dist/providers/coding-proxy/proxy.d.ts +12 -2
- package/dist/providers/coding-proxy/proxy.js +92 -30
- package/dist/providers/coding-proxy/types.d.ts +18 -1
- package/dist/providers/coding-proxy/types.js +12 -1
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/executor/composition.js +9 -1
- package/dist/providers/executor/container.d.ts +30 -2
- package/dist/providers/executor/container.js +98 -17
- package/dist/providers/executor/dbos.d.ts +2 -0
- package/dist/providers/executor/dbos.js +7 -5
- package/dist/providers/executor/routing.d.ts +6 -0
- package/dist/providers/executor/routing.js +5 -0
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/issue-tracker/adf.d.ts +31 -0
- package/dist/providers/issue-tracker/adf.js +181 -0
- package/dist/providers/issue-tracker/index.d.ts +5 -0
- package/dist/providers/issue-tracker/index.js +12 -0
- package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
- package/dist/providers/issue-tracker/jira-client.js +151 -0
- package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
- package/dist/providers/issue-tracker/jira-events.js +98 -0
- package/dist/providers/issue-tracker/jira.d.ts +116 -0
- package/dist/providers/issue-tracker/jira.js +502 -0
- package/dist/providers/issue-tracker/types.d.ts +269 -0
- package/dist/providers/issue-tracker/types.js +16 -0
- package/dist/providers/jobs/docker.d.ts +5 -1
- package/dist/providers/jobs/docker.js +61 -33
- package/dist/providers/jobs/kubernetes.d.ts +3 -0
- package/dist/providers/jobs/kubernetes.js +44 -4
- package/dist/providers/jobs/service-state.d.ts +22 -0
- package/dist/providers/jobs/service-state.js +17 -0
- package/dist/providers/llm/anthropic.d.ts +3 -3
- package/dist/providers/llm/anthropic.js +3 -9
- package/dist/providers/llm/bedrock.d.ts +3 -3
- package/dist/providers/llm/bedrock.js +3 -9
- package/dist/providers/llm/catalog-lookup.d.ts +10 -0
- package/dist/providers/llm/catalog-lookup.js +15 -0
- package/dist/providers/llm/catalog-shipped.d.ts +18 -0
- package/dist/providers/llm/catalog-shipped.js +197 -0
- package/dist/providers/llm/catalog-store.d.ts +58 -0
- package/dist/providers/llm/catalog-store.js +138 -0
- package/dist/providers/llm/catalog-types.d.ts +66 -0
- package/dist/providers/llm/catalog-types.js +64 -0
- package/dist/providers/llm/catalog.d.ts +61 -0
- package/dist/providers/llm/catalog.js +147 -0
- package/dist/providers/llm/claude-provider.d.ts +10 -14
- package/dist/providers/llm/claude-provider.js +11 -6
- package/dist/providers/llm/index.d.ts +9 -6
- package/dist/providers/llm/index.js +8 -5
- package/dist/providers/llm/openai.d.ts +14 -5
- package/dist/providers/llm/openai.js +24 -14
- package/dist/providers/llm/pricing-core.d.ts +5 -3
- package/dist/providers/llm/registration.js +8 -12
- package/dist/providers/llm/routing.d.ts +18 -17
- package/dist/providers/llm/routing.js +40 -24
- package/dist/providers/review-host/github-events.js +47 -1
- package/dist/providers/review-host/github.js +7 -6
- package/dist/providers/review-host/types.d.ts +25 -0
- package/dist/providers/vcs/git.js +2 -22
- package/dist/providers/vcs/github.d.ts +20 -0
- package/dist/providers/vcs/github.js +28 -2
- package/dist/providers/vcs/types.d.ts +6 -0
- package/dist/quickstart/index.d.ts +8 -0
- package/dist/quickstart/index.js +34 -34
- package/dist/serve.js +8 -2
- package/dist/viewer/api-schema.d.ts +2757 -0
- package/dist/viewer/api-schema.js +165 -0
- package/dist/viewer/build-schemas.d.ts +2 -0
- package/dist/viewer/build-schemas.js +18 -0
- package/dist/viewer/event-bus.d.ts +38 -0
- package/dist/viewer/event-bus.js +232 -0
- package/dist/viewer/graph.d.ts +40 -0
- package/dist/viewer/graph.js +243 -0
- package/dist/viewer/http.d.ts +30 -0
- package/dist/viewer/http.js +133 -0
- package/dist/viewer/run-detail.d.ts +4 -0
- package/dist/viewer/run-detail.js +61 -0
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +10 -0
- package/docs/agent-recipes.md +383 -0
- package/docs/code-review-agents.md +29 -2
- package/docs/coding-agent-setup.md +3 -0
- package/docs/coding-worker-isolation.md +39 -5
- package/docs/getting-started-gke.md +28 -11
- package/docs/getting-started-identity-provider.md +49 -38
- package/docs/getting-started.md +14 -0
- package/docs/jira-agents.md +649 -0
- package/docs/knowledge.md +387 -0
- package/docs/models.md +221 -0
- package/docs/security-deployment.md +19 -9
- package/docs/viewer-api.md +142 -0
- package/help/admin-viewer.md +39 -0
- package/help/agent-recipes.md +173 -0
- package/help/architecture-agent.md +189 -0
- package/help/builder-agent.md +80 -0
- package/help/code-review-agents.md +6 -0
- package/help/cost-attribution.md +67 -0
- package/help/creating-agents.md +22 -0
- package/help/deploy-gke.md +6 -0
- package/help/errors/model-unavailable.md +63 -0
- package/help/getting-started.md +1 -0
- package/help/github.md +18 -0
- package/help/identity-and-access.md +8 -3
- package/help/jira.md +135 -0
- package/help/knowledge.md +47 -0
- package/help/models.md +90 -0
- package/help/operating-agents.md +7 -1
- package/help/troubleshooting/budgets.md +6 -0
- package/package.json +5 -2
- package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
- package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
- package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
- package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
- package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
- package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
- package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
- package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
- package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
- package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
- package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
- package/prisma/schema.prisma +258 -2
- package/dist/mcp/tools/models.d.ts +0 -8
- package/dist/mcp/tools/models.js +0 -15
- package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
- package/dist/providers/llm/pricing-anthropic.js +0 -48
- package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
- package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
- package/dist/providers/llm/pricing.d.ts +0 -30
- package/dist/providers/llm/pricing.js +0 -74
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run, per-model usage by priced token kind (RunModelUsage). Native runs
|
|
3
|
+
* use one model (Agent.model), written once at finish; coding runs are
|
|
4
|
+
* recomputed from the proxy ledger (prisma-ledger.ts). Always set, never
|
|
5
|
+
* incremented, so a repeat is harmless. Best effort: Run.costUsd stays the
|
|
6
|
+
* budget's source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import type { PrismaClient } from "#prisma";
|
|
9
|
+
import type { EngineResult } from "../providers/engine/types.js";
|
|
10
|
+
export declare function recordNativeModelUsage(db: Pick<PrismaClient, "runModelUsage">, runId: string, model: string, usage: EngineResult["usage"]): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
const log = logger.child({ module: "model-usage" });
|
|
3
|
+
export async function recordNativeModelUsage(db, runId, model, usage) {
|
|
4
|
+
if (usage.tokensIn === 0 && usage.tokensOut === 0 && usage.costUsd === 0)
|
|
5
|
+
return;
|
|
6
|
+
const cached = usage.cachedInputTokens ?? 0;
|
|
7
|
+
const values = {
|
|
8
|
+
freshInputTokens: Math.max(0, usage.tokensIn - cached),
|
|
9
|
+
cachedInputTokens: cached,
|
|
10
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
11
|
+
outputTokens: usage.tokensOut,
|
|
12
|
+
costUsd: usage.costUsd,
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
await db.runModelUsage.upsert({
|
|
16
|
+
where: { runId_model: { runId, model } },
|
|
17
|
+
create: { runId, model, ...values },
|
|
18
|
+
update: values,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
log.warn({ err, runId, model }, "could not record the run's model usage");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -31,13 +31,15 @@
|
|
|
31
31
|
* Each pass also completes review-host checks left "in progress" by a run
|
|
32
32
|
* that ended without reaching `executeRun`'s own finalizer (reaped here as
|
|
33
33
|
* `lost`, failed to start, failed while loading, ...). One sweep covers all
|
|
34
|
-
* of those paths instead of patching each. Mention status comments
|
|
35
|
-
*
|
|
34
|
+
* of those paths instead of patching each. Mention status comments, and the
|
|
35
|
+
* status comments of runs started by Jira issue events, get the same sweep:
|
|
36
|
+
* it also catches a run that ended before its comment was posted.
|
|
36
37
|
*/
|
|
37
38
|
import type { PrismaClient } from "#prisma";
|
|
38
39
|
import type { Executor } from "../providers/executor/types.js";
|
|
39
40
|
import type { ReviewHostRegistry } from "../providers/review-host/types.js";
|
|
40
|
-
|
|
41
|
+
import type { IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
|
|
42
|
+
export type ReconcilerDb = Pick<PrismaClient, "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "agentIssueProject" | "issuePullRequest" | "codingRun" | "agent" | "$transaction" | "$queryRaw">;
|
|
41
43
|
/**
|
|
42
44
|
* How long after a run finishes before its still-open check counts as
|
|
43
45
|
* orphaned: long enough that the sweep never races `executeRun`'s own
|
|
@@ -66,7 +68,7 @@ export declare function closeOrphanedHostChecks(db: Pick<PrismaClient, "runHostC
|
|
|
66
68
|
*/
|
|
67
69
|
export declare function closeOrphanedHostStatuses(db: Pick<PrismaClient, "runHostStatus" | "run">, hosts: ReviewHostRegistry | undefined, now?: Date): Promise<void>;
|
|
68
70
|
/** Runs one reconciliation pass. Returns the number of runs marked `lost`. */
|
|
69
|
-
export declare function reconcileOnce(db: ReconcilerDb, now?: Date, heartbeatTimeoutMs?: number, executor?: Executor, reviewHosts?: ReviewHostRegistry): Promise<number>;
|
|
71
|
+
export declare function reconcileOnce(db: ReconcilerDb, now?: Date, heartbeatTimeoutMs?: number, executor?: Executor, reviewHosts?: ReviewHostRegistry, issueTrackers?: IssueTrackerRegistry): Promise<number>;
|
|
70
72
|
export interface ReconcilerOptions {
|
|
71
73
|
db?: ReconcilerDb;
|
|
72
74
|
intervalMs?: number;
|
|
@@ -74,6 +76,8 @@ export interface ReconcilerOptions {
|
|
|
74
76
|
executor?: Executor;
|
|
75
77
|
/** Hosts used to complete checks orphaned by runs that ended abnormally; none configured = no sweep. */
|
|
76
78
|
reviewHosts?: ReviewHostRegistry;
|
|
79
|
+
/** Trackers used to complete issue status comments orphaned the same way; none configured = no sweep. */
|
|
80
|
+
issueTrackers?: IssueTrackerRegistry;
|
|
77
81
|
}
|
|
78
82
|
export interface ReconcilerHandle {
|
|
79
83
|
stop(): void;
|
package/dist/core/reconciler.js
CHANGED
|
@@ -31,15 +31,20 @@
|
|
|
31
31
|
* Each pass also completes review-host checks left "in progress" by a run
|
|
32
32
|
* that ended without reaching `executeRun`'s own finalizer (reaped here as
|
|
33
33
|
* `lost`, failed to start, failed while loading, ...). One sweep covers all
|
|
34
|
-
* of those paths instead of patching each. Mention status comments
|
|
35
|
-
*
|
|
34
|
+
* of those paths instead of patching each. Mention status comments, and the
|
|
35
|
+
* status comments of runs started by Jira issue events, get the same sweep:
|
|
36
|
+
* it also catches a run that ended before its comment was posted.
|
|
36
37
|
*/
|
|
37
38
|
import { HEARTBEAT_TIMEOUT_MS, RECONCILE_INTERVAL_MS } from "./timing.js";
|
|
38
39
|
import { prisma as defaultDb } from "./db.js";
|
|
39
40
|
import { logger } from "./logger.js";
|
|
40
41
|
import { closeOpenHostCheck } from "./review-host-checks.js";
|
|
41
42
|
import { completeHostStatus } from "./host-status.js";
|
|
43
|
+
import { closeOrphanedIssueStatuses } from "./issue-status.js";
|
|
44
|
+
import { fileSelfDefect } from "./self-defects.js";
|
|
42
45
|
const reconcilerLog = logger.child({ module: "reconciler" });
|
|
46
|
+
/** A pass waits this long per self-defect, then moves on; the filing finishes (or logs) in the background. */
|
|
47
|
+
const SELF_DEFECT_WAIT = { waitMs: 2_000 };
|
|
43
48
|
/**
|
|
44
49
|
* How long after a run finishes before its still-open check counts as
|
|
45
50
|
* orphaned: long enough that the sweep never races `executeRun`'s own
|
|
@@ -115,7 +120,7 @@ export async function closeOrphanedHostStatuses(db, hosts, now = new Date()) {
|
|
|
115
120
|
await completeHostStatus(db, run, hosts, { postIfMissing: true });
|
|
116
121
|
}
|
|
117
122
|
/** Runs one reconciliation pass. Returns the number of runs marked `lost`. */
|
|
118
|
-
export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, executor, reviewHosts) {
|
|
123
|
+
export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, executor, reviewHosts, issueTrackers) {
|
|
119
124
|
const cutoff = new Date(now.getTime() - heartbeatTimeoutMs);
|
|
120
125
|
const stale = {
|
|
121
126
|
executionManaged: true,
|
|
@@ -207,6 +212,8 @@ export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = H
|
|
|
207
212
|
data: { status: "lost", error: reason, finishedAt: now },
|
|
208
213
|
});
|
|
209
214
|
lost += result.count;
|
|
215
|
+
if (result.count > 0)
|
|
216
|
+
await fileSelfDefect(db, issueTrackers, { ...run, status: "lost", error: reason, finishedAt: now }, SELF_DEFECT_WAIT);
|
|
210
217
|
continue;
|
|
211
218
|
}
|
|
212
219
|
const result = await db.run.updateMany({
|
|
@@ -214,9 +221,13 @@ export async function reconcileOnce(db, now = new Date(), heartbeatTimeoutMs = H
|
|
|
214
221
|
data: { status: "lost", error: reason, finishedAt: now },
|
|
215
222
|
});
|
|
216
223
|
lost += result.count;
|
|
224
|
+
// Only after this pass made the row lost (not when another instance won the race); bounded, never throws.
|
|
225
|
+
if (result.count > 0)
|
|
226
|
+
await fileSelfDefect(db, issueTrackers, { ...run, status: "lost", error: reason, finishedAt: now }, SELF_DEFECT_WAIT);
|
|
217
227
|
}
|
|
218
228
|
await closeOrphanedHostChecks(db, reviewHosts, now);
|
|
219
229
|
await closeOrphanedHostStatuses(db, reviewHosts, now);
|
|
230
|
+
await closeOrphanedIssueStatuses(db, issueTrackers, now);
|
|
220
231
|
return lost;
|
|
221
232
|
}
|
|
222
233
|
export function startReconciler(options = {}) {
|
|
@@ -224,7 +235,7 @@ export function startReconciler(options = {}) {
|
|
|
224
235
|
const intervalMs = options.intervalMs ?? RECONCILE_INTERVAL_MS;
|
|
225
236
|
const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? HEARTBEAT_TIMEOUT_MS;
|
|
226
237
|
const timer = setInterval(() => {
|
|
227
|
-
reconcileOnce(db, new Date(), heartbeatTimeoutMs, options.executor, options.reviewHosts).catch((err) => {
|
|
238
|
+
reconcileOnce(db, new Date(), heartbeatTimeoutMs, options.executor, options.reviewHosts, options.issueTrackers).catch((err) => {
|
|
228
239
|
reconcilerLog.error({ err }, "reconcile pass failed");
|
|
229
240
|
});
|
|
230
241
|
}, intervalMs);
|
|
@@ -272,14 +272,21 @@ export async function handleReviewHostTool(name, argsJson, ctx) {
|
|
|
272
272
|
}
|
|
273
273
|
case "repo_read_file": {
|
|
274
274
|
const a = ReadFileArgs.parse(parsed);
|
|
275
|
-
|
|
275
|
+
const read = await host.readFile(link.repository, a.path, a.ref, {
|
|
276
276
|
startLine: a.startLine ?? 1,
|
|
277
277
|
maxLines: a.maxLines ?? 400,
|
|
278
|
-
})
|
|
278
|
+
});
|
|
279
|
+
// `text` is the raw window for machine callers; agents get only the numbered `content`.
|
|
280
|
+
if (read.kind === "file") {
|
|
281
|
+
const { text: _text, ...shown } = read;
|
|
282
|
+
return JSON.stringify(shown);
|
|
283
|
+
}
|
|
284
|
+
return JSON.stringify(read);
|
|
279
285
|
}
|
|
280
286
|
case "repo_list_files": {
|
|
281
287
|
const a = ListFilesArgs.parse(parsed);
|
|
282
|
-
|
|
288
|
+
const { ref, count, truncated, files } = await host.listFiles(link.repository, a.ref, a.pathPrefix);
|
|
289
|
+
return JSON.stringify({ ref, count, truncated, files });
|
|
283
290
|
}
|
|
284
291
|
case "repo_publish_review": {
|
|
285
292
|
const a = PublishArgs.parse(parsed);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run is billed at the model catalog entry recorded when it started, for
|
|
3
|
+
* its whole life: a resumed attempt (crash, pod move, reconciler adoption)
|
|
4
|
+
* reads the entry back from its Run row instead of the live catalog, so an
|
|
5
|
+
* admin's later set_model / disable_model never changes a run already under
|
|
6
|
+
* way. Runs from before the catalog have no stored entry and use the current
|
|
7
|
+
* catalog (identical to the shipped prices they started with).
|
|
8
|
+
*/
|
|
9
|
+
import type { Prisma } from "#prisma";
|
|
10
|
+
import type { LlmProvider } from "../providers/llm/types.js";
|
|
11
|
+
import { type CatalogEntry, type ResolvedCatalogEntry } from "../providers/llm/catalog-types.js";
|
|
12
|
+
import type { ModelCatalog } from "../providers/llm/catalog.js";
|
|
13
|
+
import { type CodingProvider } from "../coding/provider.js";
|
|
14
|
+
export interface PinnedPricing {
|
|
15
|
+
entry: CatalogEntry;
|
|
16
|
+
priceVersion: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RunPricingDb {
|
|
19
|
+
run: {
|
|
20
|
+
updateMany(args: {
|
|
21
|
+
where: {
|
|
22
|
+
id: string;
|
|
23
|
+
pricingVersion: null;
|
|
24
|
+
};
|
|
25
|
+
data: {
|
|
26
|
+
pricingVersion: string;
|
|
27
|
+
pricingSnapshot: Prisma.InputJsonValue;
|
|
28
|
+
};
|
|
29
|
+
}): Promise<{
|
|
30
|
+
count: number;
|
|
31
|
+
}>;
|
|
32
|
+
findUnique(args: {
|
|
33
|
+
where: {
|
|
34
|
+
id: string;
|
|
35
|
+
};
|
|
36
|
+
select: {
|
|
37
|
+
pricingVersion: true;
|
|
38
|
+
pricingSnapshot: true;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<{
|
|
41
|
+
pricingVersion: string | null;
|
|
42
|
+
pricingSnapshot: unknown;
|
|
43
|
+
} | null>;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function pinNativeRunPricing(db: RunPricingDb, run: {
|
|
47
|
+
id: string;
|
|
48
|
+
pricingVersion: string | null;
|
|
49
|
+
pricingSnapshot: unknown;
|
|
50
|
+
}, model: string, llm: LlmProvider): Promise<PinnedPricing | undefined>;
|
|
51
|
+
/** A coding agent's model is in the catalog but belongs to a model provider its coding provider cannot drive. */
|
|
52
|
+
export declare class CodingModelProviderMismatchError extends Error {
|
|
53
|
+
constructor(model: string, provider: CodingProvider);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A coding run's entry at dispatch: in the catalog, enabled, and of this coding provider's model provider.
|
|
57
|
+
* Throws ModelUnavailableError or CodingModelProviderMismatchError; dispatchRun turns either into a failed run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveCodingEntry(provider: CodingProvider, model: string, catalog?: ModelCatalog): ResolvedCatalogEntry;
|
|
60
|
+
/** Config-time check for create_agent/update_agent: the model must be runnable here. */
|
|
61
|
+
export declare function assertAgentModelAvailable(model: string, llm?: LlmProvider): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RoutingLlmProvider } from "../providers/llm/routing.js";
|
|
2
|
+
import { entryOf, parseStoredEntry, } from "../providers/llm/catalog-types.js";
|
|
3
|
+
import { currentModelCatalog } from "../providers/llm/catalog-store.js";
|
|
4
|
+
import { codingProviderForModelProvider } from "../coding/provider.js";
|
|
5
|
+
function stored(row) {
|
|
6
|
+
const entry = parseStoredEntry(row.pricingSnapshot);
|
|
7
|
+
return entry && row.pricingVersion ? { entry, priceVersion: row.pricingVersion } : undefined;
|
|
8
|
+
}
|
|
9
|
+
export async function pinNativeRunPricing(db, run, model, llm) {
|
|
10
|
+
const existing = stored(run);
|
|
11
|
+
if (existing)
|
|
12
|
+
return existing;
|
|
13
|
+
if (!(llm instanceof RoutingLlmProvider))
|
|
14
|
+
return undefined;
|
|
15
|
+
const resolved = llm.entryFor(model); // throws ModelUnavailableError before any spend
|
|
16
|
+
const entry = entryOf(resolved);
|
|
17
|
+
const { count } = await db.run.updateMany({
|
|
18
|
+
where: { id: run.id, pricingVersion: null },
|
|
19
|
+
// entryOf returns plain JSON data (fresh array, no Dates); the interface's readonly
|
|
20
|
+
// efforts array is all that keeps it from matching Prisma's Json input type.
|
|
21
|
+
data: { pricingVersion: resolved.priceVersion, pricingSnapshot: entry },
|
|
22
|
+
});
|
|
23
|
+
if (count === 1)
|
|
24
|
+
return { entry, priceVersion: resolved.priceVersion };
|
|
25
|
+
// Another attempt of this run recorded first: bill at what it recorded.
|
|
26
|
+
const row = await db.run.findUnique({
|
|
27
|
+
where: { id: run.id },
|
|
28
|
+
select: { pricingVersion: true, pricingSnapshot: true },
|
|
29
|
+
});
|
|
30
|
+
return (row && stored(row)) ?? { entry, priceVersion: resolved.priceVersion };
|
|
31
|
+
}
|
|
32
|
+
/** A coding agent's model is in the catalog but belongs to a model provider its coding provider cannot drive. */
|
|
33
|
+
export class CodingModelProviderMismatchError extends Error {
|
|
34
|
+
constructor(model, provider) {
|
|
35
|
+
super(`Model "${model}" is not supported by coding provider "${provider}".`);
|
|
36
|
+
this.name = "CodingModelProviderMismatchError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A coding run's entry at dispatch: in the catalog, enabled, and of this coding provider's model provider.
|
|
41
|
+
* Throws ModelUnavailableError or CodingModelProviderMismatchError; dispatchRun turns either into a failed run.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveCodingEntry(provider, model, catalog = currentModelCatalog()) {
|
|
44
|
+
const entry = catalog.require(model);
|
|
45
|
+
if (codingProviderForModelProvider(entry.provider) !== provider) {
|
|
46
|
+
throw new CodingModelProviderMismatchError(model, provider);
|
|
47
|
+
}
|
|
48
|
+
return entry;
|
|
49
|
+
}
|
|
50
|
+
/** Config-time check for create_agent/update_agent: the model must be runnable here. */
|
|
51
|
+
export function assertAgentModelAvailable(model, llm) {
|
|
52
|
+
if (llm instanceof RoutingLlmProvider)
|
|
53
|
+
llm.entryFor(model);
|
|
54
|
+
else
|
|
55
|
+
currentModelCatalog().require(model);
|
|
56
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { ProviderRegistry } from "../providers/index.js";
|
|
|
21
21
|
import { type StepRunner } from "../providers/engine/types.js";
|
|
22
22
|
import type { Executor } from "../providers/executor/types.js";
|
|
23
23
|
import type { ReviewHostRegistry } from "../providers/review-host/types.js";
|
|
24
|
+
import type { IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
|
|
24
25
|
import { type RepoAccessGate } from "./repo-access.js";
|
|
25
26
|
/**
|
|
26
27
|
* The subset of the Prisma client the runner touches — mockable in tests.
|
|
@@ -28,11 +29,13 @@ import { type RepoAccessGate } from "./repo-access.js";
|
|
|
28
29
|
* also satisfies `dispatch.ts`'s `DispatchDb`, needed for dispatching a
|
|
29
30
|
* coding-kind sub-agent from inside a running native turn loop.
|
|
30
31
|
*/
|
|
31
|
-
export type RunnerDb = Pick<PrismaClient, "agent" | "run" | "agentTool" | "agentSecret" | "agentDatastore" | "budgetGroup" | "agentSubAgent" | "resourceGrant" | "codingRun" | "task" | "webhook" | "$transaction" | "$queryRaw" | "agentRepository" | "runHostCheck" | "runHostStatus" | "hostIdentity">;
|
|
32
|
-
/** The providers a native run needs; `executor` and `
|
|
32
|
+
export type RunnerDb = Pick<PrismaClient, "agent" | "run" | "agentTool" | "agentSecret" | "agentDatastore" | "budgetGroup" | "agentSubAgent" | "resourceGrant" | "codingRun" | "task" | "webhook" | "$transaction" | "$queryRaw" | "agentRepository" | "runModelUsage" | "runHostCheck" | "runHostStatus" | "hostIdentity" | "agentIssueProject" | "issuePullRequest" | "runIssueStatus" | "issueFingerprint">;
|
|
33
|
+
/** The providers a native run needs; `executor`, `reviewHosts` and `issueTrackers` are optional capabilities. */
|
|
33
34
|
export type NativeRunProviders = Pick<ProviderRegistry, "llm" | "engine" | "datastore" | "secrets" | "memory"> & {
|
|
34
35
|
executor?: ProviderRegistry["executor"];
|
|
35
36
|
reviewHosts?: ReviewHostRegistry;
|
|
37
|
+
/** Issue trackers (Jira): the jira_* built-ins and issue status comments. */
|
|
38
|
+
issueTrackers?: IssueTrackerRegistry;
|
|
36
39
|
/** Repository authorization for repo_* calls; built from reviewHosts when absent. */
|
|
37
40
|
repoAccess?: RepoAccessGate;
|
|
38
41
|
};
|
package/dist/core/runner.js
CHANGED
|
@@ -27,7 +27,7 @@ import { buildSecretsAccessor, scopeSecretsAccessor } from "./secrets.js";
|
|
|
27
27
|
import { buildSharedDatastoreAccessor, scopeSharedDatastoreAccessor } from "./datastores.js";
|
|
28
28
|
import { effectiveBudgetForRun } from "./budget-groups.js";
|
|
29
29
|
import { withRunHeartbeat } from "./run-heartbeat.js";
|
|
30
|
-
import { dispatchRun } from "./dispatch.js";
|
|
30
|
+
import { ContinuationRefusedError, dispatchRun } from "./dispatch.js";
|
|
31
31
|
import { canDelegate } from "./grants.js";
|
|
32
32
|
import { MEMORY_TOOL_DEFS, MEMORY_TOOL_NAMES, handleMemoryTool } from "./memory-tools.js";
|
|
33
33
|
import { DELEGATE_TOOL_PREFIX, duplicateToolNames } from "./tool-names.js";
|
|
@@ -40,6 +40,14 @@ import { closeOpenHostCheck } from "./review-host-checks.js";
|
|
|
40
40
|
import { serviceRefusalSentence } from "../coding/services/wording.js";
|
|
41
41
|
import { RUN_TASK_TAG, splitTaskOverride, wrapUntrusted } from "./untrusted-content.js";
|
|
42
42
|
import { completeHostStatus } from "./host-status.js";
|
|
43
|
+
import { ISSUE_TRACKER_TOOL_DEFS, ISSUE_TRACKER_TOOL_NAMES, handleIssueTrackerTool, } from "./issue-tracker-tools.js";
|
|
44
|
+
import { completeIssueStatus } from "./issue-status.js";
|
|
45
|
+
import { fileIssue } from "./issue-dedupe.js";
|
|
46
|
+
import { fileSelfDefect } from "./self-defects.js";
|
|
47
|
+
import { recordNativeModelUsage } from "./model-usage.js";
|
|
48
|
+
import { pinNativeRunPricing } from "./run-pricing.js";
|
|
49
|
+
import { RoutingLlmProvider } from "../providers/llm/routing.js";
|
|
50
|
+
import { ModelUnavailableError } from "../providers/llm/catalog-types.js";
|
|
43
51
|
import { trackRun } from "./in-flight-runs.js";
|
|
44
52
|
import { createRepoAccessGate, requiredLevel } from "./repo-access.js";
|
|
45
53
|
const runnerLog = logger.child({ module: "runner" });
|
|
@@ -115,6 +123,30 @@ const DelegateArgs = z
|
|
|
115
123
|
function configuredReviewHosts(hosts) {
|
|
116
124
|
return hosts && Object.values(hosts).some(Boolean) ? hosts : undefined;
|
|
117
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The composed issue trackers, or undefined when there are none (no Jira site
|
|
128
|
+
* configured yields an empty registry). Undefined means the run never touches
|
|
129
|
+
* the AgentIssueProject/RunIssueStatus tables at all.
|
|
130
|
+
*/
|
|
131
|
+
function configuredIssueTrackers(trackers) {
|
|
132
|
+
return trackers && Object.values(trackers).some(Boolean) ? trackers : undefined;
|
|
133
|
+
}
|
|
134
|
+
/** An AgentIssueProject row as the jira_* tools see it. */
|
|
135
|
+
function toIssueProjectLink(row) {
|
|
136
|
+
return {
|
|
137
|
+
provider: "jira",
|
|
138
|
+
projectKey: row.projectKey,
|
|
139
|
+
access: row.access === "write" ? "write" : "read",
|
|
140
|
+
commentVisibilityRole: row.commentVisibilityRole,
|
|
141
|
+
// `?? []` (fail closed): a row or pinned load from before the allowlists existed allows nothing.
|
|
142
|
+
allowedTransitions: row.allowedTransitions ?? [],
|
|
143
|
+
writableFields: row.writableFields ?? [],
|
|
144
|
+
allowedLinkTypes: row.allowedLinkTypes ?? [],
|
|
145
|
+
creatableIssueTypes: row.creatableIssueTypes ?? [],
|
|
146
|
+
// No built-in cap: absent (an older pinned load) means none, like null.
|
|
147
|
+
maxNewIssuesPerRun: row.maxNewIssuesPerRun ?? null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
118
150
|
/**
|
|
119
151
|
* The two states a Run can still be driven out of. Every write `executeRun`
|
|
120
152
|
* makes is filtered on these: a durable executor can have two attempts of the
|
|
@@ -143,8 +175,12 @@ export class RunCancelledError extends Error {
|
|
|
143
175
|
* the race, the winner's if it did not.
|
|
144
176
|
*/
|
|
145
177
|
async function finishRun(db, runId, data) {
|
|
146
|
-
await db
|
|
147
|
-
|
|
178
|
+
return (await finishRunClaimed(db, runId, data)).run;
|
|
179
|
+
}
|
|
180
|
+
/** finishRun, also saying whether this call made the row terminal (false: something else finished it first). */
|
|
181
|
+
async function finishRunClaimed(db, runId, data) {
|
|
182
|
+
const updated = await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data });
|
|
183
|
+
return { run: await db.run.findUniqueOrThrow({ where: { id: runId } }), claimed: updated.count > 0 };
|
|
148
184
|
}
|
|
149
185
|
/**
|
|
150
186
|
* Why a coding run cannot start in this process: runs reach the runner only
|
|
@@ -256,16 +292,23 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
256
292
|
const repoAccess = reviewHosts
|
|
257
293
|
? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
|
|
258
294
|
: undefined;
|
|
295
|
+
const issueTrackers = configuredIssueTrackers(providers.issueTrackers);
|
|
259
296
|
// Pinned in one checkpointed step: on replay after a crash, the agent row
|
|
260
297
|
// or its budget group may have changed since first execution. The
|
|
261
298
|
// engine's control flow depends on budgetUsd and maxTurns, so they must
|
|
262
299
|
// be pinned to the values seen on first execution or the replay's step
|
|
263
300
|
// order diverges from the record.
|
|
264
|
-
const
|
|
301
|
+
const loadedOrUnavailable = await step("load", async () => {
|
|
265
302
|
const agent = await db.agent.findUnique({ where: { id: existingRun.agentId } });
|
|
266
303
|
if (!agent) {
|
|
267
304
|
throw new Error(`Run "${runId}" references missing agent "${existingRun.agentId}".`);
|
|
268
305
|
}
|
|
306
|
+
// The run's catalog entry, recorded on first execution and read back on
|
|
307
|
+
// every replay or resume, so its prices never move under it (run-pricing.ts).
|
|
308
|
+
// A model that is missing, disabled, or has no configured provider fails
|
|
309
|
+
// the run here, before any spend. Coding agents never run in this engine
|
|
310
|
+
// (failed below) and are priced by the coding proxy, so they pin nothing.
|
|
311
|
+
const pricing = agent.kind === "coding" ? undefined : await pinNativeRunPricing(db, existingRun, agent.model, providers.llm);
|
|
269
312
|
const attached = await db.agentTool.findMany({
|
|
270
313
|
where: { agentId: agent.id },
|
|
271
314
|
include: { tool: true },
|
|
@@ -301,6 +344,10 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
301
344
|
checkName: l.checkName,
|
|
302
345
|
}))
|
|
303
346
|
: [];
|
|
347
|
+
// Likewise only when a Jira site is configured.
|
|
348
|
+
const issueProjectLinks = issueTrackers
|
|
349
|
+
? (await db.agentIssueProject.findMany({ where: { agentId: agent.id, provider: "jira" } })).map(toIssueProjectLink)
|
|
350
|
+
: [];
|
|
304
351
|
const isDispatchedChild = existingRun.parentRunId != null;
|
|
305
352
|
// Appended, never prepended: the loaded systemPrompt's stable prefix
|
|
306
353
|
// stays prompt-cache-eligible across every dispatch, even though the
|
|
@@ -325,9 +372,11 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
325
372
|
return {
|
|
326
373
|
agentId: agent.id,
|
|
327
374
|
kind: agent.kind,
|
|
375
|
+
pricing,
|
|
328
376
|
memoryEnabled: agent.memoryEnabled,
|
|
329
377
|
subAgentEdges,
|
|
330
378
|
repositoryLinks,
|
|
379
|
+
issueProjectLinks,
|
|
331
380
|
agent: {
|
|
332
381
|
systemPrompt,
|
|
333
382
|
model: agent.model,
|
|
@@ -360,6 +409,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
360
409
|
...(isDispatchedChild ? [PARENT_MEMORY_GET_TOOL] : []),
|
|
361
410
|
...subAgentEdges.map((edge) => delegateToolDef(edge.boundName)),
|
|
362
411
|
...(repositoryLinks.length > 0 ? REVIEW_HOST_TOOL_DEFS : []),
|
|
412
|
+
...(issueProjectLinks.length > 0 ? ISSUE_TRACKER_TOOL_DEFS : []),
|
|
363
413
|
],
|
|
364
414
|
// An attachment's four capability fields are honoured only when the
|
|
365
415
|
// agent's CURRENT owner granted them (resource-sharing grants spec
|
|
@@ -381,14 +431,33 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
381
431
|
];
|
|
382
432
|
})),
|
|
383
433
|
};
|
|
434
|
+
}).catch((err) => {
|
|
435
|
+
// Returned, not thrown, so the run is marked failed below rather than left pending for a retry
|
|
436
|
+
// that would fail the same way. Matched by message too: a DBOS replay may hand back a
|
|
437
|
+
// deserialized error that is no longer a ModelUnavailableError instance.
|
|
438
|
+
if (err instanceof ModelUnavailableError ||
|
|
439
|
+
(err instanceof Error && err.message.startsWith("model_unavailable:"))) {
|
|
440
|
+
return { unavailable: err.message };
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
384
443
|
});
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
444
|
+
// Fails a run that ends before its engine starts, closing what its trigger opened (the review
|
|
445
|
+
// check, the host and issue status comments) exactly as the normal and catch paths below do.
|
|
446
|
+
// Deliberately no self-defect: both callers are configuration states, not this run's defect (an
|
|
447
|
+
// admin disabled or removed the model, or this deployment has no coding executor), and filing
|
|
448
|
+
// would open one defect per affected agent rather than describe a failure of that agent.
|
|
449
|
+
const finishEarly = async (error) => {
|
|
450
|
+
const finished = await finishRun(db, runId, { status: "failed", error, finishedAt: new Date() });
|
|
451
|
+
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
452
|
+
await completeHostStatus(db, finished, reviewHosts);
|
|
453
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
454
|
+
return finished;
|
|
455
|
+
};
|
|
456
|
+
if ("unavailable" in loadedOrUnavailable)
|
|
457
|
+
return finishEarly(loadedOrUnavailable.unavailable);
|
|
458
|
+
const loaded = loadedOrUnavailable;
|
|
459
|
+
if (loaded.kind === "coding")
|
|
460
|
+
return finishEarly(CODING_EXECUTOR_NOT_CONFIGURED);
|
|
392
461
|
// Conditional on DRIVABLE rather than on `pending`: an adopted attempt
|
|
393
462
|
// legitimately finds the row already `running`, but a terminal row must
|
|
394
463
|
// never be flipped back to `running`.
|
|
@@ -397,6 +466,9 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
397
466
|
const toolsByName = new Map(Object.entries(loaded.toolsByName));
|
|
398
467
|
const secretsAccessor = buildSecretsAccessor(loaded.agentId, providers.secrets, db);
|
|
399
468
|
const sharedDatastoreAccessor = buildSharedDatastoreAccessor(loaded.agentId, providers.datastore, db);
|
|
469
|
+
// jira_create_issue's per-run cap counter: shared by every tool call of this attempt (a resumed attempt
|
|
470
|
+
// starts a fresh one, floored by the run's recorded fingerprint creates).
|
|
471
|
+
const issueCreationCounters = new Map();
|
|
400
472
|
const runSandboxTool = async (name, argsJson) => {
|
|
401
473
|
if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
|
|
402
474
|
return handleMemoryTool(name, argsJson, loaded.agentId, providers.memory);
|
|
@@ -449,6 +521,30 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
449
521
|
},
|
|
450
522
|
});
|
|
451
523
|
}
|
|
524
|
+
// `?? []`: a load step replayed from before these links were pinned has none.
|
|
525
|
+
// Each link is re-normalised too: one pinned before the allowlists existed lacks them.
|
|
526
|
+
const issueProjectLinks = (loaded.issueProjectLinks ?? []).map(toIssueProjectLink);
|
|
527
|
+
if (ISSUE_TRACKER_TOOL_NAMES.has(name) && issueProjectLinks.length > 0 && issueTrackers) {
|
|
528
|
+
return handleIssueTrackerTool(name, argsJson, {
|
|
529
|
+
agentId: loaded.agentId,
|
|
530
|
+
links: issueProjectLinks,
|
|
531
|
+
trackers: issueTrackers,
|
|
532
|
+
// Live, not from the pinned load: an unlink, downgrade, or new
|
|
533
|
+
// visibility role takes effect on the very next call.
|
|
534
|
+
currentLink: async (projectKey) => {
|
|
535
|
+
const row = await db.agentIssueProject.findUnique({
|
|
536
|
+
where: { agentId_provider_projectKey: { agentId: loaded.agentId, provider: "jira", projectKey } },
|
|
537
|
+
});
|
|
538
|
+
return row ? toIssueProjectLink(row) : null;
|
|
539
|
+
},
|
|
540
|
+
creation: {
|
|
541
|
+
runId,
|
|
542
|
+
counters: issueCreationCounters,
|
|
543
|
+
fileIssue: (input) => fileIssue({ db }, input),
|
|
544
|
+
recordedCreates: (projectKey) => db.issueFingerprint.count({ where: { createdByRunId: runId, issueProvider: "jira", projectKey } }),
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
452
548
|
if (name === "subagent_memory_get") {
|
|
453
549
|
return handleSubAgentMemoryGet(argsJson, loaded.agentId, db, providers.memory);
|
|
454
550
|
}
|
|
@@ -567,19 +663,35 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
567
663
|
// dispatchRun reserves the child's budget itself, inside its persist
|
|
568
664
|
// transaction: the agent's budgetUsd tightened by its budget group and
|
|
569
665
|
// by this run tree (parentRunId), or refused when either is spent.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
666
|
+
let dispatched;
|
|
667
|
+
try {
|
|
668
|
+
dispatched = await dispatchRun({
|
|
669
|
+
db,
|
|
670
|
+
executor: providers.executor,
|
|
671
|
+
selfDefects: { db, issueTrackers },
|
|
672
|
+
agentId: edge.childAgentId,
|
|
673
|
+
trigger: "subagent",
|
|
674
|
+
codingTask: args.task,
|
|
675
|
+
continuesCodingRunId: args.continuePriorRun,
|
|
676
|
+
parentRunId: runId,
|
|
677
|
+
// The child's result flows back into this run, which the
|
|
678
|
+
// triggerer sees, so the child is visible to them too.
|
|
679
|
+
triggeredById: existingRun.triggeredById,
|
|
680
|
+
awaitExecution: true,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
// A continuation this deployment cannot make (the run id came from
|
|
685
|
+
// a pull request another deployment opened, say) is the model's to
|
|
686
|
+
// report, not a reason to fail the whole run.
|
|
687
|
+
if (!(err instanceof ContinuationRefusedError))
|
|
688
|
+
throw err;
|
|
689
|
+
return JSON.stringify({
|
|
690
|
+
error: "continuation_refused",
|
|
691
|
+
message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
|
|
692
|
+
"tell the requester that this wardby deployment cannot continue that pull request's branch.",
|
|
693
|
+
});
|
|
694
|
+
}
|
|
583
695
|
if (!dispatched) {
|
|
584
696
|
return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
|
|
585
697
|
}
|
|
@@ -676,15 +788,42 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
676
788
|
}
|
|
677
789
|
return JSON.stringify(result.value);
|
|
678
790
|
};
|
|
791
|
+
// Live progress for observers (MCP get_run, the viewer). Absolute totals, so a DBOS replay
|
|
792
|
+
// re-writing them is harmless. Written into the run's own cost columns on purpose: the run
|
|
793
|
+
// tree's shared budget (computeRunTreeSpend) then counts a running parent's spend so far, as it
|
|
794
|
+
// already does for coding runs, whose proxy ledger writes their totals live. Best-effort: a
|
|
795
|
+
// failed write only delays what observers see, and finishRun writes the final totals anyway.
|
|
796
|
+
const onProgress = async (progress) => {
|
|
797
|
+
try {
|
|
798
|
+
await db.run.updateMany({
|
|
799
|
+
where: { id: runId, status: "running" },
|
|
800
|
+
data: {
|
|
801
|
+
turns: progress.turns,
|
|
802
|
+
tokensIn: progress.usage.tokensIn,
|
|
803
|
+
tokensOut: progress.usage.tokensOut,
|
|
804
|
+
costUsd: progress.usage.costUsd,
|
|
805
|
+
heartbeatAt: new Date(),
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
runnerLog.warn({ err, runId }, "failed to record run progress");
|
|
811
|
+
}
|
|
812
|
+
};
|
|
679
813
|
const engineResult = await providers.engine.run({
|
|
680
814
|
agent: loaded.agent,
|
|
681
815
|
tools: loaded.tools,
|
|
682
|
-
providers: {
|
|
816
|
+
providers: {
|
|
817
|
+
llm: loaded.pricing && providers.llm instanceof RoutingLlmProvider
|
|
818
|
+
? providers.llm.forRun(loaded.pricing.entry)
|
|
819
|
+
: providers.llm,
|
|
820
|
+
},
|
|
683
821
|
runSandboxTool,
|
|
684
822
|
onText,
|
|
823
|
+
onProgress,
|
|
685
824
|
step,
|
|
686
825
|
});
|
|
687
|
-
const finished = await
|
|
826
|
+
const { run: finished, claimed } = await finishRunClaimed(db, runId, {
|
|
688
827
|
status: engineResult.status,
|
|
689
828
|
tokensIn: engineResult.usage.tokensIn,
|
|
690
829
|
tokensOut: engineResult.usage.tokensOut,
|
|
@@ -694,8 +833,15 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
694
833
|
turns: engineResult.turns,
|
|
695
834
|
finishedAt: new Date(),
|
|
696
835
|
});
|
|
836
|
+
// Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
|
|
837
|
+
await recordNativeModelUsage(db, runId, loaded.agent.model, engineResult.usage);
|
|
697
838
|
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
698
839
|
await completeHostStatus(db, finished, reviewHosts);
|
|
840
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
841
|
+
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
842
|
+
// filed twice. Bounded and never throws.
|
|
843
|
+
if (claimed)
|
|
844
|
+
await fileSelfDefect(db, issueTrackers, finished);
|
|
699
845
|
return finished;
|
|
700
846
|
}
|
|
701
847
|
catch (err) {
|
|
@@ -704,13 +850,18 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
704
850
|
// (a real bug, or tool-loading failing outside the per-tool try above)
|
|
705
851
|
// must still never leave the run dangling in "running". A cancellation
|
|
706
852
|
// is not a failure: it carries the operator's own reason.
|
|
707
|
-
const finished = await
|
|
853
|
+
const { run: finished, claimed } = await finishRunClaimed(db, runId, {
|
|
708
854
|
status: err instanceof RunCancelledError ? "cancelled" : "failed",
|
|
709
855
|
error: err instanceof Error ? err.message : String(err),
|
|
710
856
|
finishedAt: new Date(),
|
|
711
857
|
});
|
|
712
858
|
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
713
859
|
await completeHostStatus(db, finished, reviewHosts);
|
|
860
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
861
|
+
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
862
|
+
// filed twice. Bounded and never throws.
|
|
863
|
+
if (claimed)
|
|
864
|
+
await fileSelfDefect(db, issueTrackers, finished);
|
|
714
865
|
return finished;
|
|
715
866
|
}
|
|
716
867
|
}
|