@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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost by issue / parent / scope / agent / model / run over a time window
|
|
3
|
+
* (docs/private/2026-10-01-issue-cost-attribution-design.md §7). Rows count
|
|
4
|
+
* attributed runs only; unattributed spend is reported separately so the gap
|
|
5
|
+
* is visible. Money is summed as NUMERIC in SQL and returned as strings; tokens
|
|
6
|
+
* are always by priced kind, never one total.
|
|
7
|
+
*/
|
|
8
|
+
import { Prisma } from "#prisma";
|
|
9
|
+
const GROUP_BYS = ["issue", "parent", "scope", "agent", "model", "run"];
|
|
10
|
+
const DEFAULT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
11
|
+
const DEFAULT_LIMIT = 25;
|
|
12
|
+
const MAX_LIMIT = 200;
|
|
13
|
+
export class CostReportInputError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "CostReportInputError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function optionalString(args, name) {
|
|
20
|
+
const v = args[name];
|
|
21
|
+
if (v === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (typeof v !== "string" || v.length === 0 || v.length > 256) {
|
|
24
|
+
throw new CostReportInputError(`${name} must be a non-empty string.`);
|
|
25
|
+
}
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
function optionalDate(args, name) {
|
|
29
|
+
const v = args[name];
|
|
30
|
+
if (v === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const d = typeof v === "string" ? new Date(v) : new Date(NaN);
|
|
33
|
+
if (Number.isNaN(d.getTime()))
|
|
34
|
+
throw new CostReportInputError(`${name} must be an ISO date.`);
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
export function parseCostReportQuery(raw, now = new Date()) {
|
|
38
|
+
const args = raw && typeof raw === "object" ? raw : {};
|
|
39
|
+
const groupBy = (args.groupBy ?? "issue");
|
|
40
|
+
if (!GROUP_BYS.includes(groupBy)) {
|
|
41
|
+
throw new CostReportInputError(`groupBy must be one of: ${GROUP_BYS.join(", ")}.`);
|
|
42
|
+
}
|
|
43
|
+
const to = optionalDate(args, "to") ?? now;
|
|
44
|
+
const from = optionalDate(args, "from") ?? new Date(to.getTime() - DEFAULT_WINDOW_MS);
|
|
45
|
+
if (from.getTime() >= to.getTime())
|
|
46
|
+
throw new CostReportInputError("from must be before to.");
|
|
47
|
+
let limit = DEFAULT_LIMIT;
|
|
48
|
+
if (args.limit !== undefined) {
|
|
49
|
+
if (typeof args.limit !== "number" || !Number.isInteger(args.limit) || args.limit < 1) {
|
|
50
|
+
throw new CostReportInputError("limit must be a positive integer.");
|
|
51
|
+
}
|
|
52
|
+
limit = Math.min(args.limit, MAX_LIMIT);
|
|
53
|
+
}
|
|
54
|
+
const q = { groupBy, from, to, limit };
|
|
55
|
+
for (const name of ["provider", "scopeKey", "parentKey", "issueKey", "agentId"]) {
|
|
56
|
+
const v = optionalString(args, name);
|
|
57
|
+
if (v !== undefined)
|
|
58
|
+
q[name] = v;
|
|
59
|
+
}
|
|
60
|
+
return q;
|
|
61
|
+
}
|
|
62
|
+
const NO_PARENT = "(no parent)";
|
|
63
|
+
/** NUMERIC text → canonical decimal string without trailing zeros ("3.100000" → "3.1", null → "0"). */
|
|
64
|
+
function usd(v) {
|
|
65
|
+
if (v == null)
|
|
66
|
+
return "0";
|
|
67
|
+
return v.includes(".") ? v.replace(/0+$/, "").replace(/\.$/, "") : v;
|
|
68
|
+
}
|
|
69
|
+
const kinds = (r) => ({
|
|
70
|
+
freshInput: Number(r?.fresh ?? 0),
|
|
71
|
+
cachedInput: Number(r?.cached ?? 0),
|
|
72
|
+
cacheWrite: Number(r?.write ?? 0),
|
|
73
|
+
output: Number(r?.out ?? 0),
|
|
74
|
+
});
|
|
75
|
+
/** The run filter every query shares: window, visibility, and the agent filter. Only `r` must be in scope. */
|
|
76
|
+
function runFilter(q, vis) {
|
|
77
|
+
const parts = [Prisma.sql `r."startedAt" >= ${q.from} AND r."startedAt" < ${q.to}`];
|
|
78
|
+
if (vis) {
|
|
79
|
+
parts.push(vis.ownedAgentIds.length > 0
|
|
80
|
+
? Prisma.sql `(r."agentId" IN (${Prisma.join(vis.ownedAgentIds)}) OR r."triggeredById" = ${vis.principalId})`
|
|
81
|
+
: Prisma.sql `r."triggeredById" = ${vis.principalId}`);
|
|
82
|
+
}
|
|
83
|
+
if (q.agentId)
|
|
84
|
+
parts.push(Prisma.sql `r."agentId" = ${q.agentId}`);
|
|
85
|
+
return Prisma.join(parts, " AND ");
|
|
86
|
+
}
|
|
87
|
+
/** The work-item filters; `a` (RunAttribution) and `w` (WorkItem) must be joined. */
|
|
88
|
+
function attributionFilter(q) {
|
|
89
|
+
const parts = [Prisma.sql `TRUE`];
|
|
90
|
+
if (q.provider)
|
|
91
|
+
parts.push(Prisma.sql `w."provider" = ${q.provider}`);
|
|
92
|
+
if (q.scopeKey)
|
|
93
|
+
parts.push(Prisma.sql `w."scopeKey" = ${q.scopeKey}`);
|
|
94
|
+
if (q.parentKey)
|
|
95
|
+
parts.push(Prisma.sql `a."parentKeyAtRun" = ${q.parentKey}`);
|
|
96
|
+
if (q.issueKey)
|
|
97
|
+
parts.push(Prisma.sql `w."key" = ${q.issueKey}`);
|
|
98
|
+
return Prisma.join(parts, " AND ");
|
|
99
|
+
}
|
|
100
|
+
/** The group key for each groupBy, over Run r / RunAttribution a / WorkItem w (and RunModelUsage u for "model"). */
|
|
101
|
+
const GROUP_KEY = {
|
|
102
|
+
issue: Prisma.sql `w."key"`,
|
|
103
|
+
parent: Prisma.sql `COALESCE(a."parentKeyAtRun", '(no parent)')`,
|
|
104
|
+
scope: Prisma.sql `w."scopeKey"`,
|
|
105
|
+
agent: Prisma.sql `r."agentId"`,
|
|
106
|
+
model: Prisma.sql `u."model"`,
|
|
107
|
+
run: Prisma.sql `r."id"`,
|
|
108
|
+
};
|
|
109
|
+
const ATTRIBUTED = Prisma.sql `"Run" r JOIN "RunAttribution" a ON a."runId" = r."id" JOIN "WorkItem" w ON w."id" = a."workItemId"`;
|
|
110
|
+
const USAGE_ATTRIBUTED = Prisma.sql `"RunModelUsage" u JOIN ${ATTRIBUTED} ON r."id" = u."runId"`;
|
|
111
|
+
/**
|
|
112
|
+
* How long one report may take. Prisma's interactive-transaction default (5 s)
|
|
113
|
+
* is too short for a wide window over a large Run table; the same limit is set
|
|
114
|
+
* as the transaction's statement_timeout so Postgres stops the work too, not
|
|
115
|
+
* only the client.
|
|
116
|
+
*/
|
|
117
|
+
export const COST_REPORT_TIMEOUT_MS = 30_000;
|
|
118
|
+
/**
|
|
119
|
+
* Every query runs in one read-only REPEATABLE READ transaction, so the rows,
|
|
120
|
+
* the totals and the unattributed figure all come from the same snapshot even
|
|
121
|
+
* while runs are finishing.
|
|
122
|
+
*/
|
|
123
|
+
export async function costReport(db, q, visibility) {
|
|
124
|
+
return db.$transaction(async (tx) => {
|
|
125
|
+
await tx.$executeRaw `SET TRANSACTION READ ONLY`;
|
|
126
|
+
// SET takes no bind parameters; the value is a constant.
|
|
127
|
+
await tx.$executeRawUnsafe(`SET LOCAL statement_timeout = '${COST_REPORT_TIMEOUT_MS}ms'`);
|
|
128
|
+
return reportIn(tx, q, visibility);
|
|
129
|
+
}, { isolationLevel: "RepeatableRead", timeout: COST_REPORT_TIMEOUT_MS });
|
|
130
|
+
}
|
|
131
|
+
async function reportIn(db, q, visibility) {
|
|
132
|
+
const where = Prisma.sql `${runFilter(q, visibility)} AND ${attributionFilter(q)}`;
|
|
133
|
+
const key = GROUP_KEY[q.groupBy];
|
|
134
|
+
// "model" groups usage rows, so its money comes from RunModelUsage; every other grouping sums Run.costUsd once per run.
|
|
135
|
+
const byModel = q.groupBy === "model";
|
|
136
|
+
const money = byModel ? Prisma.sql `u."costUsd"` : Prisma.sql `r."costUsd"`;
|
|
137
|
+
const source = byModel ? USAGE_ATTRIBUTED : ATTRIBUTED;
|
|
138
|
+
const groups = await db.$queryRaw `
|
|
139
|
+
SELECT ${key} AS key,
|
|
140
|
+
MIN(w."provider") AS provider,
|
|
141
|
+
MIN(w."parentKind") AS parent_kind,
|
|
142
|
+
COUNT(DISTINCT r."id") AS runs,
|
|
143
|
+
COUNT(DISTINCT r."id") FILTER (WHERE r."status" IN ('pending', 'running')) AS in_progress,
|
|
144
|
+
SUM(${money})::text AS usd,
|
|
145
|
+
SUM(${money}) FILTER (WHERE a."source" = 'issue_event')::text AS src_issue_event,
|
|
146
|
+
SUM(${money}) FILTER (WHERE a."source" = 'linked_pr')::text AS src_linked_pr,
|
|
147
|
+
SUM(${money}) FILTER (WHERE a."source" = 'explicit')::text AS src_explicit,
|
|
148
|
+
SUM(${money}) FILTER (WHERE a."source" = 'inherited')::text AS src_inherited,
|
|
149
|
+
MIN(r."startedAt") AS first_at,
|
|
150
|
+
MAX(r."startedAt") AS last_at
|
|
151
|
+
FROM ${source}
|
|
152
|
+
WHERE ${where}
|
|
153
|
+
GROUP BY ${key}
|
|
154
|
+
ORDER BY SUM(${money}) DESC NULLS LAST, ${key}
|
|
155
|
+
LIMIT ${q.limit + 1}`;
|
|
156
|
+
const truncated = groups.length > q.limit;
|
|
157
|
+
const page = groups.slice(0, q.limit);
|
|
158
|
+
// Per-group, per-model tokens and money for just this page's groups.
|
|
159
|
+
const usage = page.length
|
|
160
|
+
? await db.$queryRaw `
|
|
161
|
+
SELECT ${key} AS key, u."model" AS model, SUM(u."costUsd")::text AS usd,
|
|
162
|
+
SUM(u."freshInputTokens") AS fresh, SUM(u."cachedInputTokens") AS cached,
|
|
163
|
+
SUM(u."cacheWriteTokens") AS write, SUM(u."outputTokens") AS out
|
|
164
|
+
FROM ${USAGE_ATTRIBUTED}
|
|
165
|
+
WHERE ${where} AND ${key} IN (${Prisma.join(page.map((g) => g.key))})
|
|
166
|
+
GROUP BY ${key}, u."model"
|
|
167
|
+
ORDER BY SUM(u."costUsd") DESC, u."model"`
|
|
168
|
+
: [];
|
|
169
|
+
const titles = await groupTitles(db, q.groupBy, page);
|
|
170
|
+
const rows = page.map((g) => {
|
|
171
|
+
const models = usage
|
|
172
|
+
.filter((u) => u.key === g.key)
|
|
173
|
+
.map((m) => ({ model: m.model, costUsd: usd(m.usd), tokens: kinds(m) }));
|
|
174
|
+
const tokens = models.reduce((t, m) => ({
|
|
175
|
+
freshInput: t.freshInput + m.tokens.freshInput,
|
|
176
|
+
cachedInput: t.cachedInput + m.tokens.cachedInput,
|
|
177
|
+
cacheWrite: t.cacheWrite + m.tokens.cacheWrite,
|
|
178
|
+
output: t.output + m.tokens.output,
|
|
179
|
+
}), { freshInput: 0, cachedInput: 0, cacheWrite: 0, output: 0 });
|
|
180
|
+
return {
|
|
181
|
+
key: g.key,
|
|
182
|
+
...titles(g),
|
|
183
|
+
provider: g.provider,
|
|
184
|
+
runs: Number(g.runs),
|
|
185
|
+
inProgressRuns: Number(g.in_progress),
|
|
186
|
+
costUsd: usd(g.usd),
|
|
187
|
+
tokens,
|
|
188
|
+
byModel: models,
|
|
189
|
+
bySource: {
|
|
190
|
+
issue_event: usd(g.src_issue_event),
|
|
191
|
+
linked_pr: usd(g.src_linked_pr),
|
|
192
|
+
explicit: usd(g.src_explicit),
|
|
193
|
+
inherited: usd(g.src_inherited),
|
|
194
|
+
},
|
|
195
|
+
firstRunAt: g.first_at.toISOString(),
|
|
196
|
+
lastRunAt: g.last_at.toISOString(),
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
const [totals] = await db.$queryRaw `
|
|
200
|
+
SELECT COUNT(*) AS runs, SUM(r."costUsd")::text AS usd FROM ${ATTRIBUTED} WHERE ${where}`;
|
|
201
|
+
const [totalTokens] = await db.$queryRaw `
|
|
202
|
+
SELECT SUM(u."freshInputTokens") AS fresh, SUM(u."cachedInputTokens") AS cached,
|
|
203
|
+
SUM(u."cacheWriteTokens") AS write, SUM(u."outputTokens") AS out
|
|
204
|
+
FROM ${USAGE_ATTRIBUTED} WHERE ${where}`;
|
|
205
|
+
const [unattributed] = await db.$queryRaw `
|
|
206
|
+
SELECT COUNT(*) AS runs, SUM(r."costUsd")::text AS usd
|
|
207
|
+
FROM "Run" r
|
|
208
|
+
WHERE ${runFilter(q, visibility)}
|
|
209
|
+
AND NOT EXISTS (SELECT 1 FROM "RunAttribution" ra WHERE ra."runId" = r."id")`;
|
|
210
|
+
return {
|
|
211
|
+
currency: "USD",
|
|
212
|
+
groupBy: q.groupBy,
|
|
213
|
+
from: q.from.toISOString(),
|
|
214
|
+
to: q.to.toISOString(),
|
|
215
|
+
rows,
|
|
216
|
+
truncated,
|
|
217
|
+
totals: { runs: Number(totals?.runs ?? 0), costUsd: usd(totals?.usd), tokens: kinds(totalTokens) },
|
|
218
|
+
unattributed: { runs: Number(unattributed?.runs ?? 0), costUsd: usd(unattributed?.usd) },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Titles and kinds per group: current WorkItem names for issue/parent, agent names for agent; none otherwise. */
|
|
222
|
+
async function groupTitles(db, groupBy, page) {
|
|
223
|
+
const none = () => ({ title: null, kind: null });
|
|
224
|
+
if (page.length === 0)
|
|
225
|
+
return none;
|
|
226
|
+
if (groupBy === "issue" || groupBy === "parent") {
|
|
227
|
+
const keys = page.map((g) => g.key).filter((k) => k !== NO_PARENT);
|
|
228
|
+
if (keys.length === 0)
|
|
229
|
+
return none;
|
|
230
|
+
const items = await db.$queryRaw `
|
|
231
|
+
SELECT "provider", "key", "title", "type" FROM "WorkItem" WHERE "key" IN (${Prisma.join(keys)})`;
|
|
232
|
+
const byKey = new Map(items.map((i) => [`${i.provider}\u0000${i.key}`, i]));
|
|
233
|
+
return (g) => {
|
|
234
|
+
if (groupBy === "parent" && g.key === NO_PARENT)
|
|
235
|
+
return { title: null, kind: null };
|
|
236
|
+
const i = byKey.get(`${g.provider}\u0000${g.key}`);
|
|
237
|
+
// A parent's kind lives on its children (WorkItem.parentKind), selected in the group query.
|
|
238
|
+
return { title: i?.title ?? null, kind: groupBy === "parent" ? g.parent_kind : (i?.type ?? null) };
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (groupBy === "agent") {
|
|
242
|
+
const agents = await db.$queryRaw `
|
|
243
|
+
SELECT "id", "name" FROM "Agent" WHERE "id" IN (${Prisma.join(page.map((g) => g.key))})`;
|
|
244
|
+
const byId = new Map(agents.map((a) => [a.id, a.name]));
|
|
245
|
+
return (g) => ({ title: byId.get(g.key) ?? null, kind: "agent" });
|
|
246
|
+
}
|
|
247
|
+
return none;
|
|
248
|
+
}
|
package/dist/core/dispatch.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Prisma, PrismaClient, Run, RunTrigger, Task } from "#prisma";
|
|
2
2
|
import type { Executor } from "../providers/executor/types.js";
|
|
3
|
+
import { type AttributionIntent } from "./attribution.js";
|
|
3
4
|
import { type BudgetConstraint } from "./budget-groups.js";
|
|
5
|
+
import { type SelfDefectSink } from "./self-defects.js";
|
|
4
6
|
export type DispatchDb = Pick<PrismaClient, "agent" | "run" | "runHostCheck" | "codingRun" | "task" | "webhook" | "budgetGroup" | "$transaction" | "$queryRaw">;
|
|
5
|
-
export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "run" | "runHostCheck" | "runHostStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "$queryRaw" | "$executeRawUnsafe">;
|
|
7
|
+
export type DispatchTx = Pick<Prisma.TransactionClient, "agent" | "run" | "runHostCheck" | "runHostStatus" | "runIssueStatus" | "codingRun" | "codingService" | "task" | "webhook" | "budgetGroup" | "resourceGrant" | "workItem" | "runAttribution" | "$queryRaw" | "$executeRawUnsafe">;
|
|
6
8
|
type DispatchAgent = Prisma.AgentGetPayload<{
|
|
7
9
|
include: {
|
|
8
10
|
codingProfile: true;
|
|
@@ -39,6 +41,8 @@ export interface DispatchRunOptions {
|
|
|
39
41
|
* can observe them (e.g. RunHostCheck, see core/host-events.ts).
|
|
40
42
|
*/
|
|
41
43
|
afterPersist?: (tx: DispatchTx, run: Run) => Promise<void>;
|
|
44
|
+
/** Files a self-defect when the executor fails to start the run and this marks it failed. Optional. */
|
|
45
|
+
selfDefects?: SelfDefectSink;
|
|
42
46
|
/**
|
|
43
47
|
* Who the run is visible to besides the agent owner (Run.triggeredById,
|
|
44
48
|
* resource-sharing grants spec §3.5): the trigger_agent caller, a
|
|
@@ -48,6 +52,13 @@ export interface DispatchRunOptions {
|
|
|
48
52
|
triggeredById?: string | null;
|
|
49
53
|
/** Sub-agent dispatch (see AgentSubAgent): links this run into a run tree. */
|
|
50
54
|
parentRunId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Cost attribution for a run that starts a new attribution (issue event,
|
|
57
|
+
* linked PR, explicit key), resolved before the call (attribution.ts
|
|
58
|
+
* resolveWorkItem). Ignored when the parent run or the continued coding run
|
|
59
|
+
* is attributed: inheritance wins.
|
|
60
|
+
*/
|
|
61
|
+
attribution?: AttributionIntent;
|
|
51
62
|
grantedParentMemoryKeys?: string[];
|
|
52
63
|
/**
|
|
53
64
|
* Per-run task text for a NATIVE agent (appended to its systemPrompt at
|
|
@@ -88,6 +99,10 @@ export interface DispatchRunResult {
|
|
|
88
99
|
* (Prisma 7) -- Prisma 6's engine put 40001 at meta.code, still accepted;
|
|
89
100
|
* - at COMMIT (e.g. SSI write skew): an unwrapped DriverAdapterError, no
|
|
90
101
|
* code or meta, with the SQLSTATE at cause.originalCode.
|
|
102
|
+
* Also a unique violation (P2002) on WorkItem: concurrent first dispatches
|
|
103
|
+
* attributed to one new issue each insert its WorkItem, and Postgres reports
|
|
104
|
+
* the loser as 23505 rather than 40001. The retry finds the committed row.
|
|
105
|
+
* Unique violations on other models are real errors and are not retried.
|
|
91
106
|
*
|
|
92
107
|
* @internal Exported only for the real-PostgreSQL tests.
|
|
93
108
|
*/
|
|
@@ -100,7 +115,7 @@ export declare function isSerializationConflict(err: unknown): boolean;
|
|
|
100
115
|
* the conditional `updateMany` makes the second call match zero rows rather
|
|
101
116
|
* than overwrite whatever landed in between.
|
|
102
117
|
*/
|
|
103
|
-
export declare function markRunFailedFromExecutorError(db: Pick<DispatchDb, "run">, runId: string, err: unknown): Promise<void>;
|
|
118
|
+
export declare function markRunFailedFromExecutorError(db: Pick<DispatchDb, "run">, runId: string, err: unknown, selfDefects?: SelfDefectSink): Promise<void>;
|
|
104
119
|
/**
|
|
105
120
|
* The `Run.error` of a coding run refused at dispatch because a budget
|
|
106
121
|
* constraint has nothing left: `budget_group_exhausted:<period>` or
|
|
@@ -114,6 +129,31 @@ export declare function budgetExhaustedError(constraint: BudgetConstraint): stri
|
|
|
114
129
|
* @internal Exported only for the real-PostgreSQL tests.
|
|
115
130
|
*/
|
|
116
131
|
export declare const PERSIST_ATTEMPTS = 8;
|
|
132
|
+
export type ContinuationRefusal = "unknown_run" | "other_repository" | "no_pull_request";
|
|
133
|
+
/** A continuation refused because of the run it names. Its message is fixed text, safe to show the model. */
|
|
134
|
+
export declare class ContinuationRefusedError extends Error {
|
|
135
|
+
readonly reason: ContinuationRefusal;
|
|
136
|
+
constructor(reason: ContinuationRefusal);
|
|
137
|
+
}
|
|
138
|
+
export type ContinuationCheck = {
|
|
139
|
+
ok: true;
|
|
140
|
+
root: {
|
|
141
|
+
runId: string;
|
|
142
|
+
baseRef: string;
|
|
143
|
+
headRef: string;
|
|
144
|
+
pullRequestNumber: number;
|
|
145
|
+
};
|
|
146
|
+
} | {
|
|
147
|
+
ok: false;
|
|
148
|
+
reason: ContinuationRefusal;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Whether this deployment can continue coding run `runId` in `repository`: the
|
|
152
|
+
* run (or its root) is in this database, in that repository, and opened a pull
|
|
153
|
+
* request. A run id read from a pull request can name a run that another
|
|
154
|
+
* deployment sharing the same App made, so check before relying on one.
|
|
155
|
+
*/
|
|
156
|
+
export declare function checkContinuation(reader: Pick<DispatchTx, "codingRun">, runId: string, repository: string): Promise<ContinuationCheck>;
|
|
117
157
|
/**
|
|
118
158
|
* Persists every detached run input in one transaction, then invokes the
|
|
119
159
|
* executor only after commit. A null result means the caller's transactional
|
package/dist/core/dispatch.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { CODING_PROTOCOL_VERSION, CodingTaskInputSchema, normalizeGitHubRepository, normalizeGitRef, publicCodingRunResult, composeCodingTask, } from "../coding/protocol.js";
|
|
2
|
-
import { assertCodingProviderModel } from "../coding/provider.js";
|
|
2
|
+
import { assertCodingProvider, assertCodingProviderModel } from "../coding/provider.js";
|
|
3
|
+
import { ModelUnavailableError, entryOf } from "../providers/llm/catalog-types.js";
|
|
3
4
|
import { parseAllowedServiceNames, workerServices } from "../coding/services/catalog.js";
|
|
4
5
|
import { MAX_SERVICE_DECLARATION_BYTES, ServiceDeclarationError, parseServiceDeclaration, } from "../coding/services/declaration.js";
|
|
5
6
|
import { servicesInstructionNote } from "../coding/services/note.js";
|
|
6
7
|
import { resolveRunServices } from "../coding/services/resolve.js";
|
|
7
8
|
import { DECLARATION_UNAVAILABLE_SENTENCE, LAUNCHER_UNSUPPORTED_SENTENCE, invalidDeclarationSentence, serviceRefusal, } from "../coding/services/wording.js";
|
|
9
|
+
import { attributeRun } from "./attribution.js";
|
|
8
10
|
import { effectiveBudgetForRun, MIN_RESERVATION_USD } from "./budget-groups.js";
|
|
11
|
+
import { DEFAULT_KNOWLEDGE_BUNDLE_PATH } from "../knowledge/concept.js";
|
|
12
|
+
import { KNOWLEDGE_INDEX_READ_MAX_BYTES, knowledgeSection } from "../knowledge/note.js";
|
|
9
13
|
import { logger } from "./logger.js";
|
|
14
|
+
import { CodingModelProviderMismatchError, resolveCodingEntry } from "./run-pricing.js";
|
|
15
|
+
import { fileSelfDefectForRun } from "./self-defects.js";
|
|
10
16
|
const dispatchLog = logger.child({ module: "dispatch" });
|
|
11
17
|
/** SQLSTATEs worth retrying the persist transaction for: serialization failure, deadlock. */
|
|
12
18
|
const RETRYABLE_SQLSTATES = new Set(["40001", "40P01"]);
|
|
@@ -21,6 +27,10 @@ const RETRYABLE_SQLSTATES = new Set(["40001", "40P01"]);
|
|
|
21
27
|
* (Prisma 7) -- Prisma 6's engine put 40001 at meta.code, still accepted;
|
|
22
28
|
* - at COMMIT (e.g. SSI write skew): an unwrapped DriverAdapterError, no
|
|
23
29
|
* code or meta, with the SQLSTATE at cause.originalCode.
|
|
30
|
+
* Also a unique violation (P2002) on WorkItem: concurrent first dispatches
|
|
31
|
+
* attributed to one new issue each insert its WorkItem, and Postgres reports
|
|
32
|
+
* the loser as 23505 rather than 40001. The retry finds the committed row.
|
|
33
|
+
* Unique violations on other models are real errors and are not retried.
|
|
24
34
|
*
|
|
25
35
|
* @internal Exported only for the real-PostgreSQL tests.
|
|
26
36
|
*/
|
|
@@ -30,6 +40,8 @@ export function isSerializationConflict(err) {
|
|
|
30
40
|
const candidate = err;
|
|
31
41
|
if (candidate.code === "P2034")
|
|
32
42
|
return true;
|
|
43
|
+
if (candidate.code === "P2002")
|
|
44
|
+
return candidate.meta?.modelName === "WorkItem";
|
|
33
45
|
if (candidate.code === "P2010") {
|
|
34
46
|
const sqlState = candidate.meta?.driverAdapterError?.cause?.originalCode;
|
|
35
47
|
return (typeof sqlState === "string" && RETRYABLE_SQLSTATES.has(sqlState)) || candidate.meta?.code === "40001";
|
|
@@ -48,8 +60,8 @@ export function isSerializationConflict(err) {
|
|
|
48
60
|
* the conditional `updateMany` makes the second call match zero rows rather
|
|
49
61
|
* than overwrite whatever landed in between.
|
|
50
62
|
*/
|
|
51
|
-
export async function markRunFailedFromExecutorError(db, runId, err) {
|
|
52
|
-
await db.run.updateMany({
|
|
63
|
+
export async function markRunFailedFromExecutorError(db, runId, err, selfDefects) {
|
|
64
|
+
const updated = await db.run.updateMany({
|
|
53
65
|
where: { id: runId, status: { in: ["pending", "running"] } },
|
|
54
66
|
data: {
|
|
55
67
|
status: "failed",
|
|
@@ -57,6 +69,9 @@ export async function markRunFailedFromExecutorError(db, runId, err) {
|
|
|
57
69
|
finishedAt: new Date(),
|
|
58
70
|
},
|
|
59
71
|
});
|
|
72
|
+
// Only the call that made the row failed files (the duplicate call matches zero rows); bounded, never throws.
|
|
73
|
+
if (updated.count > 0)
|
|
74
|
+
await fileSelfDefectForRun(selfDefects, runId);
|
|
60
75
|
}
|
|
61
76
|
/**
|
|
62
77
|
* The `Run.error` of a coding run refused at dispatch because a budget
|
|
@@ -111,32 +126,67 @@ function retryDelayMs(attempt) {
|
|
|
111
126
|
const ceiling = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** attempt);
|
|
112
127
|
return Math.floor(ceiling / 2 + Math.random() * (ceiling / 2));
|
|
113
128
|
}
|
|
129
|
+
const CONTINUATION_REFUSALS = {
|
|
130
|
+
unknown_run: "Cannot continue an unknown coding run.",
|
|
131
|
+
other_repository: "Cannot continue a coding run from a different repository.",
|
|
132
|
+
no_pull_request: "Cannot continue a coding run that never opened a pull request.",
|
|
133
|
+
};
|
|
134
|
+
/** A continuation refused because of the run it names. Its message is fixed text, safe to show the model. */
|
|
135
|
+
export class ContinuationRefusedError extends Error {
|
|
136
|
+
reason;
|
|
137
|
+
constructor(reason) {
|
|
138
|
+
super(CONTINUATION_REFUSALS[reason]);
|
|
139
|
+
this.reason = reason;
|
|
140
|
+
this.name = "ContinuationRefusedError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
114
143
|
/**
|
|
115
|
-
*
|
|
116
|
-
* (
|
|
117
|
-
*
|
|
144
|
+
* Whether this deployment can continue coding run `runId` in `repository`: the
|
|
145
|
+
* run (or its root) is in this database, in that repository, and opened a pull
|
|
146
|
+
* request. A run id read from a pull request can name a run that another
|
|
147
|
+
* deployment sharing the same App made, so check before relying on one.
|
|
118
148
|
*/
|
|
119
|
-
async function
|
|
120
|
-
|
|
121
|
-
return { baseRef: options.codingBaseRef ?? profile.baseRef };
|
|
122
|
-
if (options.codingBaseRef !== undefined) {
|
|
123
|
-
throw new Error("continuesCodingRunId cannot be combined with codingBaseRef.");
|
|
124
|
-
}
|
|
125
|
-
const candidate = await reader.codingRun.findUnique({ where: { runId: options.continuesCodingRunId } });
|
|
149
|
+
export async function checkContinuation(reader, runId, repository) {
|
|
150
|
+
const candidate = await reader.codingRun.findUnique({ where: { runId } });
|
|
126
151
|
if (!candidate)
|
|
127
|
-
|
|
152
|
+
return { ok: false, reason: "unknown_run" };
|
|
128
153
|
const root = candidate.rootCodingRunId
|
|
129
154
|
? await reader.codingRun.findUnique({ where: { runId: candidate.rootCodingRunId } })
|
|
130
155
|
: candidate;
|
|
131
156
|
if (!root)
|
|
132
|
-
|
|
133
|
-
if (normalizeGitHubRepository(root.repository) !== normalizeGitHubRepository(
|
|
134
|
-
|
|
157
|
+
return { ok: false, reason: "unknown_run" };
|
|
158
|
+
if (normalizeGitHubRepository(root.repository) !== normalizeGitHubRepository(repository)) {
|
|
159
|
+
return { ok: false, reason: "other_repository" };
|
|
135
160
|
}
|
|
136
161
|
const rootResult = publicCodingRunResult(root.result);
|
|
137
|
-
if (rootResult?.outcome !== "pull_request_opened" && rootResult?.outcome !== "pull_request_updated")
|
|
138
|
-
|
|
162
|
+
if ((rootResult?.outcome !== "pull_request_opened" && rootResult?.outcome !== "pull_request_updated") ||
|
|
163
|
+
rootResult.pullRequestNumber === undefined) {
|
|
164
|
+
return { ok: false, reason: "no_pull_request" };
|
|
139
165
|
}
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
root: {
|
|
169
|
+
runId: root.runId,
|
|
170
|
+
baseRef: root.baseRef,
|
|
171
|
+
headRef: root.headRef,
|
|
172
|
+
pullRequestNumber: rootResult.pullRequestNumber,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolves a coding run's branch: an override, else a continuation root's
|
|
178
|
+
* (see checkContinuation), else the profile's base.
|
|
179
|
+
*/
|
|
180
|
+
async function resolveCodingBranch(reader, options, profile) {
|
|
181
|
+
if (options.continuesCodingRunId === undefined)
|
|
182
|
+
return { baseRef: options.codingBaseRef ?? profile.baseRef };
|
|
183
|
+
if (options.codingBaseRef !== undefined) {
|
|
184
|
+
throw new Error("continuesCodingRunId cannot be combined with codingBaseRef.");
|
|
185
|
+
}
|
|
186
|
+
const check = await checkContinuation(reader, options.continuesCodingRunId, profile.repository);
|
|
187
|
+
if (!check.ok)
|
|
188
|
+
throw new ContinuationRefusedError(check.reason);
|
|
189
|
+
const { root } = check;
|
|
140
190
|
return {
|
|
141
191
|
baseRef: root.baseRef,
|
|
142
192
|
headRef: root.headRef,
|
|
@@ -150,6 +200,35 @@ const UNREADABLE_DECLARATION = {
|
|
|
150
200
|
github_file_not_a_file: "it is not a file",
|
|
151
201
|
github_file_not_utf8: "it is not UTF-8 text",
|
|
152
202
|
};
|
|
203
|
+
/**
|
|
204
|
+
* The knowledge note's input (docs/knowledge.md): the repository's bundle
|
|
205
|
+
* index at the run's base, read before the transaction like the services
|
|
206
|
+
* declaration. Any failure means no note, never a failed dispatch.
|
|
207
|
+
*/
|
|
208
|
+
async function readKnowledgeIndex(options, profile, resolvedBranch) {
|
|
209
|
+
const executor = options.executor;
|
|
210
|
+
if (!executor.readCodingRepositoryFile)
|
|
211
|
+
return undefined;
|
|
212
|
+
try {
|
|
213
|
+
// Resolved here, inside the try: a branch problem is the transaction's to report, not the note's.
|
|
214
|
+
const branch = resolvedBranch ?? (await resolveCodingBranch(options.db, options, profile));
|
|
215
|
+
const indexText = await executor.readCodingRepositoryFile({
|
|
216
|
+
repository: profile.repository,
|
|
217
|
+
baseRef: normalizeGitRef(branch.baseRef),
|
|
218
|
+
path: `${DEFAULT_KNOWLEDGE_BUNDLE_PATH}/index.md`,
|
|
219
|
+
maxBytes: KNOWLEDGE_INDEX_READ_MAX_BYTES,
|
|
220
|
+
});
|
|
221
|
+
return indexText ? { bundlePath: DEFAULT_KNOWLEDGE_BUNDLE_PATH, indexText } : undefined;
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (err instanceof Error && err.message in UNREADABLE_DECLARATION) {
|
|
225
|
+
dispatchLog.info({ repository: profile.repository, reason: err.message }, "knowledge index unreadable; dispatching without it");
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
dispatchLog.warn({ err, repository: profile.repository }, "knowledge index read failed; dispatching without it");
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
153
232
|
/**
|
|
154
233
|
* Coding-run services (docs/coding-services.md): resolves the run's branch and
|
|
155
234
|
* reads the repository's declaration from its base, then parses it. A network
|
|
@@ -245,6 +324,10 @@ export async function dispatchRun(options) {
|
|
|
245
324
|
parseAllowedServiceNames(preview.codingProfile.services).length > 0
|
|
246
325
|
? await readServiceDeclaration(options, preview.codingProfile)
|
|
247
326
|
: undefined;
|
|
327
|
+
// The knowledge note's input: the repository's bundle index at the run's base (a network call).
|
|
328
|
+
const knowledgeIndex = preview?.kind === "coding" && preview.codingProfile && options.executor.readCodingRepositoryFile
|
|
329
|
+
? await readKnowledgeIndex(options, preview.codingProfile, declarationRead?.branch)
|
|
330
|
+
: undefined;
|
|
248
331
|
const persistOnce = () => options.db.$transaction(async (tx) => {
|
|
249
332
|
if (lockGroups)
|
|
250
333
|
await tx.$executeRawUnsafe(GROUP_DISPATCH_LOCK_SQL);
|
|
@@ -271,13 +354,30 @@ export async function dispatchRun(options) {
|
|
|
271
354
|
// reservation. A native run computes its effective budget in
|
|
272
355
|
// executeRun's load step instead.
|
|
273
356
|
let codingBudget;
|
|
357
|
+
let codingEntry;
|
|
358
|
+
let modelFailure;
|
|
274
359
|
let services = [];
|
|
275
360
|
let servicesRefusal;
|
|
276
361
|
if (agent.kind === "coding") {
|
|
277
362
|
if (!agent.codingProfile)
|
|
278
363
|
throw new Error(`Coding agent "${agent.id}" has no coding profile.`);
|
|
279
|
-
|
|
280
|
-
|
|
364
|
+
// An unknown coding provider is a broken profile, not a catalog state: still thrown.
|
|
365
|
+
assertCodingProvider(agent.codingProfile.provider);
|
|
366
|
+
// Recorded on the run row below: the run is billed at this entry for its whole life.
|
|
367
|
+
// A model the catalog cannot run (missing, disabled, or another provider's) fails
|
|
368
|
+
// the run here instead of throwing: the run row commits with the reason, so the
|
|
369
|
+
// caller's own writes stand (a scheduler's lastScheduledAt, a mention's status
|
|
370
|
+
// row) and the owner sees why in list_runs, rather than a rolled-back
|
|
371
|
+
// transaction that a scheduler would retry on every tick.
|
|
372
|
+
try {
|
|
373
|
+
codingEntry = resolveCodingEntry(agent.codingProfile.provider, agent.model);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
if (!(err instanceof ModelUnavailableError || err instanceof CodingModelProviderMismatchError))
|
|
377
|
+
throw err;
|
|
378
|
+
modelFailure = err.message;
|
|
379
|
+
}
|
|
380
|
+
if (!modelFailure && declarationRead) {
|
|
281
381
|
if (declarationRead.repository !== agent.codingProfile.repository ||
|
|
282
382
|
declarationRead.profileBaseRef !== agent.codingProfile.baseRef) {
|
|
283
383
|
// The declaration was read for a profile that changed before this transaction.
|
|
@@ -291,7 +391,7 @@ export async function dispatchRun(options) {
|
|
|
291
391
|
else
|
|
292
392
|
services = resolved.services;
|
|
293
393
|
}
|
|
294
|
-
if (!servicesRefusal)
|
|
394
|
+
if (!modelFailure && !servicesRefusal)
|
|
295
395
|
codingBudget = await reserveCodingBudget(tx, agent, now, options);
|
|
296
396
|
}
|
|
297
397
|
const refusal = servicesRefusal ?? codingBudget?.refusal;
|
|
@@ -304,13 +404,29 @@ export async function dispatchRun(options) {
|
|
|
304
404
|
grantedParentMemoryKeys: options.grantedParentMemoryKeys ?? [],
|
|
305
405
|
taskOverride: options.taskOverride,
|
|
306
406
|
triggeredById: options.triggeredById ?? null,
|
|
407
|
+
// Failed before it starts, like a native run whose model the
|
|
408
|
+
// runner's load step finds unavailable: zero spend, never started.
|
|
409
|
+
...(modelFailure ? { status: "failed", error: modelFailure, finishedAt: now } : {}),
|
|
307
410
|
// Refused before it starts, the same terminal status (and zero
|
|
308
411
|
// spend) as a native run whose budget is gone at turn 1.
|
|
309
412
|
...(refusal ? { status: "refused", error: refusal, finishedAt: now } : {}),
|
|
413
|
+
...(codingEntry
|
|
414
|
+
? {
|
|
415
|
+
pricingVersion: codingEntry.priceVersion,
|
|
416
|
+
// entryOf returns plain JSON data (fresh array, no Dates); the interface's readonly
|
|
417
|
+
// efforts array is all that keeps it from matching Prisma's Json input type.
|
|
418
|
+
pricingSnapshot: entryOf(codingEntry),
|
|
419
|
+
}
|
|
420
|
+
: {}),
|
|
310
421
|
},
|
|
311
422
|
});
|
|
312
423
|
if (options.afterPersist)
|
|
313
424
|
await options.afterPersist(tx, run);
|
|
425
|
+
const attributed = await attributeRun(tx, run.id, {
|
|
426
|
+
parentRunId: options.parentRunId,
|
|
427
|
+
continuesCodingRunId: options.continuesCodingRunId,
|
|
428
|
+
intent: options.attribution,
|
|
429
|
+
}, now);
|
|
314
430
|
if (agent.kind === "coding" && agent.codingProfile && codingBudget && !refusal) {
|
|
315
431
|
// Checked above too; repeated so the provider type narrows here.
|
|
316
432
|
assertCodingProviderModel(agent.codingProfile.provider, agent.model);
|
|
@@ -319,7 +435,7 @@ export async function dispatchRun(options) {
|
|
|
319
435
|
throw new Error(`Coding agent "${agent.id}" requires a task.`);
|
|
320
436
|
// The worker sees only the task text, so the agent's own instructions, and the
|
|
321
437
|
// services note, ride in it.
|
|
322
|
-
const task = composeCodingTask(agent.systemPrompt, request, servicesInstructionNote(services));
|
|
438
|
+
const task = composeCodingTask(agent.systemPrompt, request, servicesInstructionNote(services), knowledgeIndex ? knowledgeSection(knowledgeIndex) : undefined);
|
|
323
439
|
const budgetUsd = codingBudget.budgetUsd;
|
|
324
440
|
// Resolved once: before the transaction when the declaration was read from it.
|
|
325
441
|
const branch = declarationRead?.branch ?? (await resolveCodingBranch(tx, options, agent.codingProfile));
|
|
@@ -351,6 +467,8 @@ export async function dispatchRun(options) {
|
|
|
351
467
|
await tx.codingRun.create({
|
|
352
468
|
data: {
|
|
353
469
|
runId: run.id,
|
|
470
|
+
issueProvider: attributed?.provider ?? null,
|
|
471
|
+
issueKey: attributed?.key ?? null,
|
|
354
472
|
task: input.task,
|
|
355
473
|
repository: input.repository,
|
|
356
474
|
baseRef: input.baseRef,
|
|
@@ -407,8 +525,8 @@ export async function dispatchRun(options) {
|
|
|
407
525
|
}
|
|
408
526
|
if (!persisted)
|
|
409
527
|
return null;
|
|
410
|
-
if (persisted.run.status === "refused") {
|
|
411
|
-
dispatchLog.warn({ runId: persisted.run.id, agentId: options.agentId, reason: persisted.run.error },
|
|
528
|
+
if (persisted.run.status === "refused" || persisted.run.status === "failed") {
|
|
529
|
+
dispatchLog.warn({ runId: persisted.run.id, agentId: options.agentId, reason: persisted.run.error }, `coding run ${persisted.run.status} at dispatch`);
|
|
412
530
|
return persisted;
|
|
413
531
|
}
|
|
414
532
|
const runStart = async () => {
|
|
@@ -416,7 +534,7 @@ export async function dispatchRun(options) {
|
|
|
416
534
|
await options.executor.start(persisted.run.id);
|
|
417
535
|
}
|
|
418
536
|
catch (err) {
|
|
419
|
-
await markRunFailedFromExecutorError(options.db, persisted.run.id, err).catch((err2) => dispatchLog.error({ err: err2, runId: persisted.run.id }, "failed to persist executor start failure"));
|
|
537
|
+
await markRunFailedFromExecutorError(options.db, persisted.run.id, err, options.selfDefects).catch((err2) => dispatchLog.error({ err: err2, runId: persisted.run.id }, "failed to persist executor start failure"));
|
|
420
538
|
}
|
|
421
539
|
};
|
|
422
540
|
if (options.awaitExecution) {
|