@treeseed/agent 0.8.5
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/Dockerfile +7 -0
- package/README.md +198 -0
- package/dist/agent-runtime.d.ts +17 -0
- package/dist/agent-runtime.js +117 -0
- package/dist/agents/adapters/execution.d.ts +41 -0
- package/dist/agents/adapters/execution.js +73 -0
- package/dist/agents/adapters/mutations.d.ts +22 -0
- package/dist/agents/adapters/mutations.js +30 -0
- package/dist/agents/adapters/notification.d.ts +26 -0
- package/dist/agents/adapters/notification.js +46 -0
- package/dist/agents/adapters/repository.d.ts +28 -0
- package/dist/agents/adapters/repository.js +61 -0
- package/dist/agents/adapters/research.d.ts +26 -0
- package/dist/agents/adapters/research.js +59 -0
- package/dist/agents/adapters/verification.d.ts +36 -0
- package/dist/agents/adapters/verification.js +62 -0
- package/dist/agents/cli-tools.d.ts +1 -0
- package/dist/agents/cli-tools.js +5 -0
- package/dist/agents/cli.d.ts +15 -0
- package/dist/agents/cli.js +109 -0
- package/dist/agents/contracts/messages.d.ts +88 -0
- package/dist/agents/contracts/messages.js +138 -0
- package/dist/agents/contracts/run.d.ts +21 -0
- package/dist/agents/contracts/run.js +0 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.js +5 -0
- package/dist/agents/kernel/agent-kernel.d.ts +63 -0
- package/dist/agents/kernel/agent-kernel.js +291 -0
- package/dist/agents/kernel/trigger-resolver.d.ts +19 -0
- package/dist/agents/kernel/trigger-resolver.js +157 -0
- package/dist/agents/registry-helper.d.ts +4 -0
- package/dist/agents/registry-helper.js +14 -0
- package/dist/agents/registry.d.ts +6 -0
- package/dist/agents/registry.js +98 -0
- package/dist/agents/runtime-types.d.ts +118 -0
- package/dist/agents/runtime-types.js +0 -0
- package/dist/agents/spec-loader.d.ts +18 -0
- package/dist/agents/spec-loader.js +54 -0
- package/dist/agents/spec-normalizer.d.ts +2 -0
- package/dist/agents/spec-normalizer.js +327 -0
- package/dist/agents/spec-types.d.ts +64 -0
- package/dist/agents/spec-types.js +0 -0
- package/dist/agents/testing/agents-smoke.d.ts +1 -0
- package/dist/agents/testing/agents-smoke.js +32 -0
- package/dist/agents/testing/e2e-harness.d.ts +44 -0
- package/dist/agents/testing/e2e-harness.js +503 -0
- package/dist/api/agent-routes.d.ts +13 -0
- package/dist/api/agent-routes.js +327 -0
- package/dist/api/app.d.ts +8 -0
- package/dist/api/app.js +444 -0
- package/dist/api/auth/d1-database.d.ts +3 -0
- package/dist/api/auth/d1-database.js +20 -0
- package/dist/api/auth/d1-provider.d.ts +79 -0
- package/dist/api/auth/d1-provider.js +92 -0
- package/dist/api/auth/d1-store.d.ts +114 -0
- package/dist/api/auth/d1-store.js +895 -0
- package/dist/api/auth/memory-provider.d.ts +77 -0
- package/dist/api/auth/memory-provider.js +249 -0
- package/dist/api/auth/rbac.d.ts +22 -0
- package/dist/api/auth/rbac.js +162 -0
- package/dist/api/auth/tokens.d.ts +18 -0
- package/dist/api/auth/tokens.js +56 -0
- package/dist/api/capabilities.d.ts +9 -0
- package/dist/api/capabilities.js +33 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +77 -0
- package/dist/api/http.d.ts +28 -0
- package/dist/api/http.js +51 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +20 -0
- package/dist/api/operations-routes.d.ts +11 -0
- package/dist/api/operations-routes.js +87 -0
- package/dist/api/operations.d.ts +3 -0
- package/dist/api/operations.js +26 -0
- package/dist/api/project-routes.d.ts +8 -0
- package/dist/api/project-routes.js +585 -0
- package/dist/api/providers.d.ts +2 -0
- package/dist/api/providers.js +62 -0
- package/dist/api/railway.d.ts +51 -0
- package/dist/api/railway.js +71 -0
- package/dist/api/sdk-dispatch.d.ts +5 -0
- package/dist/api/sdk-dispatch.js +13 -0
- package/dist/api/sdk-routes.d.ts +11 -0
- package/dist/api/sdk-routes.js +29 -0
- package/dist/api/server.d.ts +2 -0
- package/dist/api/server.js +10 -0
- package/dist/api/templates.d.ts +3 -0
- package/dist/api/templates.js +31 -0
- package/dist/api/types.d.ts +237 -0
- package/dist/api/types.js +0 -0
- package/dist/env.yaml +957 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +41 -0
- package/dist/scripts/assert-release-tag-version.d.ts +1 -0
- package/dist/scripts/assert-release-tag-version.js +20 -0
- package/dist/scripts/build-dist.d.ts +1 -0
- package/dist/scripts/build-dist.js +106 -0
- package/dist/scripts/package-tools.d.ts +1 -0
- package/dist/scripts/package-tools.js +7 -0
- package/dist/scripts/publish-package.d.ts +1 -0
- package/dist/scripts/publish-package.js +24 -0
- package/dist/scripts/release-verify.d.ts +1 -0
- package/dist/scripts/release-verify.js +152 -0
- package/dist/scripts/test-smoke.d.ts +1 -0
- package/dist/scripts/test-smoke.js +23 -0
- package/dist/scripts/treeseed-agent-api.d.ts +2 -0
- package/dist/scripts/treeseed-agent-api.js +25 -0
- package/dist/scripts/treeseed-agent-service.d.ts +2 -0
- package/dist/scripts/treeseed-agent-service.js +36 -0
- package/dist/scripts/treeseed-agents.d.ts +2 -0
- package/dist/scripts/treeseed-agents.js +13 -0
- package/dist/services/agents.d.ts +17 -0
- package/dist/services/agents.js +48 -0
- package/dist/services/common.d.ts +66 -0
- package/dist/services/common.js +212 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +19 -0
- package/dist/services/manager.d.ts +333 -0
- package/dist/services/manager.js +1368 -0
- package/dist/services/remote-runner.d.ts +30 -0
- package/dist/services/remote-runner.js +230 -0
- package/dist/services/workday-content.d.ts +53 -0
- package/dist/services/workday-content.js +190 -0
- package/dist/services/workday-manager.d.ts +391 -0
- package/dist/services/workday-manager.js +163 -0
- package/dist/services/workday-report.d.ts +238 -0
- package/dist/services/workday-report.js +17 -0
- package/dist/services/workday-start.d.ts +238 -0
- package/dist/services/workday-start.js +17 -0
- package/dist/services/worker-capacity.d.ts +58 -0
- package/dist/services/worker-capacity.js +208 -0
- package/dist/services/worker-pool-scaler.d.ts +27 -0
- package/dist/services/worker-pool-scaler.js +127 -0
- package/dist/services/worker.d.ts +19 -0
- package/dist/services/worker.js +436 -0
- package/dist/templates/github/deploy-processing.workflow.yml +119 -0
- package/package.json +136 -0
- package/templates/github/deploy-processing.workflow.yml +119 -0
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
createControlPlaneReporter,
|
|
5
|
+
reserveCreditsForEstimate,
|
|
6
|
+
selectBestCapacityLane,
|
|
7
|
+
summarizeCapacityPlan
|
|
8
|
+
} from "@treeseed/sdk";
|
|
9
|
+
import { loadActiveAgentSpecs } from "../agents/spec-loader.js";
|
|
10
|
+
import { followCursorKey, resolveTriggerDecision } from "../agents/kernel/trigger-resolver.js";
|
|
11
|
+
import { createQueuePushClient, createServiceSdk, queueEnvelopeForTask, resolveManagerConfig, seedGraphRefreshTask } from "./common.js";
|
|
12
|
+
import {
|
|
13
|
+
applyInteractiveWakeUpOverride,
|
|
14
|
+
applyScaleCooldown,
|
|
15
|
+
collectTaskMetrics,
|
|
16
|
+
computeDesiredWorkerCount
|
|
17
|
+
} from "./worker-capacity.js";
|
|
18
|
+
import { writeWorkdayContentSnapshot } from "./workday-content.js";
|
|
19
|
+
import { createWorkerPoolScaler } from "./worker-pool-scaler.js";
|
|
20
|
+
const DEFAULT_WORK_DAYS = [1, 2, 3, 4, 5];
|
|
21
|
+
const DEFAULT_PRIORITY_MODELS = ["objective", "question", "note", "page", "book", "knowledge"];
|
|
22
|
+
function integerFromEnv(name, fallback) {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (!value) return fallback;
|
|
25
|
+
const parsed = Number.parseInt(value, 10);
|
|
26
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
27
|
+
}
|
|
28
|
+
function envValue(name) {
|
|
29
|
+
const value = process.env[name]?.trim();
|
|
30
|
+
return value ? value : "";
|
|
31
|
+
}
|
|
32
|
+
function booleanFromEnv(name, fallback = false) {
|
|
33
|
+
const value = envValue(name).toLowerCase();
|
|
34
|
+
if (!value) {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
return ["1", "true", "yes", "on"].includes(value);
|
|
38
|
+
}
|
|
39
|
+
function parseDays(value) {
|
|
40
|
+
const days = value.split(",").map((entry) => Number.parseInt(entry.trim(), 10)).filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6);
|
|
41
|
+
return days.length > 0 ? [...new Set(days)] : [...DEFAULT_WORK_DAYS];
|
|
42
|
+
}
|
|
43
|
+
function parseWindowsFromEnv() {
|
|
44
|
+
const jsonValue = envValue("TREESEED_WORKDAY_WINDOWS_JSON");
|
|
45
|
+
if (jsonValue) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(jsonValue);
|
|
48
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [{
|
|
55
|
+
days: parseDays(envValue("TREESEED_WORKDAY_DAYS") || DEFAULT_WORK_DAYS.join(",")),
|
|
56
|
+
startTime: envValue("TREESEED_WORKDAY_START_TIME") || "09:00",
|
|
57
|
+
endTime: envValue("TREESEED_WORKDAY_END_TIME") || "17:00"
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
function resolveScheduleFromEnv() {
|
|
61
|
+
return {
|
|
62
|
+
timezone: envValue("TREESEED_WORKDAY_TIMEZONE") || process.env.TZ || "UTC",
|
|
63
|
+
windows: parseWindowsFromEnv()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function parsePriorityModels() {
|
|
67
|
+
const raw = envValue("TREESEED_MANAGER_PRIORITY_MODELS");
|
|
68
|
+
if (!raw) {
|
|
69
|
+
return [...DEFAULT_PRIORITY_MODELS];
|
|
70
|
+
}
|
|
71
|
+
return raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
function parseMinutes(value) {
|
|
74
|
+
const [hours, minutes] = value.split(":", 2).map((entry) => Number.parseInt(entry, 10));
|
|
75
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
return hours * 60 + minutes;
|
|
79
|
+
}
|
|
80
|
+
function zonedNowParts(date, timezone) {
|
|
81
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
82
|
+
timeZone: timezone,
|
|
83
|
+
weekday: "short",
|
|
84
|
+
hour: "2-digit",
|
|
85
|
+
minute: "2-digit",
|
|
86
|
+
hour12: false
|
|
87
|
+
}).formatToParts(date);
|
|
88
|
+
const weekdayMap = {
|
|
89
|
+
Sun: 0,
|
|
90
|
+
Mon: 1,
|
|
91
|
+
Tue: 2,
|
|
92
|
+
Wed: 3,
|
|
93
|
+
Thu: 4,
|
|
94
|
+
Fri: 5,
|
|
95
|
+
Sat: 6
|
|
96
|
+
};
|
|
97
|
+
const weekday = weekdayMap[parts.find((part) => part.type === "weekday")?.value ?? "Sun"] ?? 0;
|
|
98
|
+
const hour = Number.parseInt(parts.find((part) => part.type === "hour")?.value ?? "0", 10);
|
|
99
|
+
const minute = Number.parseInt(parts.find((part) => part.type === "minute")?.value ?? "0", 10);
|
|
100
|
+
return {
|
|
101
|
+
weekday,
|
|
102
|
+
minutes: hour * 60 + minute
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function isWithinWorkWindow(date, schedule) {
|
|
106
|
+
const now = zonedNowParts(date, schedule.timezone);
|
|
107
|
+
for (const window of schedule.windows) {
|
|
108
|
+
const startMinutes = parseMinutes(window.startTime);
|
|
109
|
+
const endMinutes = parseMinutes(window.endTime);
|
|
110
|
+
const todayIncluded = window.days.includes(now.weekday);
|
|
111
|
+
if (startMinutes <= endMinutes) {
|
|
112
|
+
if (todayIncluded && now.minutes >= startMinutes && now.minutes <= endMinutes) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const previousDay = (now.weekday + 6) % 7;
|
|
118
|
+
if (todayIncluded && now.minutes >= startMinutes) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (window.days.includes(previousDay) && now.minutes <= endMinutes) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function parseJson(value, fallback) {
|
|
128
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(value);
|
|
133
|
+
} catch {
|
|
134
|
+
return fallback;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function asRecords(value) {
|
|
138
|
+
return Array.isArray(value) ? value : [];
|
|
139
|
+
}
|
|
140
|
+
function readString(record, ...keys) {
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
const value = record[key];
|
|
143
|
+
if (typeof value === "string" && value.trim()) {
|
|
144
|
+
return value.trim();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
function readArray(record, ...keys) {
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
const value = record[key];
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
function readNumber(record, ...keys) {
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
const value = record[key];
|
|
161
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === "string" && value.trim()) {
|
|
165
|
+
const parsed = Number.parseFloat(value);
|
|
166
|
+
if (Number.isFinite(parsed)) {
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
function readDate(record, ...keys) {
|
|
174
|
+
const raw = readString(record, ...keys);
|
|
175
|
+
if (!raw) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const parsed = new Date(raw);
|
|
179
|
+
return Number.isFinite(parsed.valueOf()) ? parsed : null;
|
|
180
|
+
}
|
|
181
|
+
function asRecord(value) {
|
|
182
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
183
|
+
}
|
|
184
|
+
function parseJsonString(value, fallback = {}) {
|
|
185
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
186
|
+
return fallback;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(value);
|
|
190
|
+
} catch {
|
|
191
|
+
return fallback;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function normalizeChangedFilesFromValue(value, changedFiles = /* @__PURE__ */ new Set()) {
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
for (const entry of value) {
|
|
197
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
198
|
+
changedFiles.add(entry.trim());
|
|
199
|
+
} else if (entry && typeof entry === "object") {
|
|
200
|
+
normalizeChangedFilesFromValue(entry, changedFiles);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return changedFiles;
|
|
204
|
+
}
|
|
205
|
+
if (!value || typeof value !== "object") {
|
|
206
|
+
return changedFiles;
|
|
207
|
+
}
|
|
208
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
209
|
+
if (["changedFiles", "changed_files", "files", "paths"].includes(key)) {
|
|
210
|
+
normalizeChangedFilesFromValue(nested, changedFiles);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (nested && typeof nested === "object") {
|
|
214
|
+
normalizeChangedFilesFromValue(nested, changedFiles);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return changedFiles;
|
|
218
|
+
}
|
|
219
|
+
function isoDateOrNull(value) {
|
|
220
|
+
if (!value) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const parsed = new Date(value);
|
|
224
|
+
return Number.isFinite(parsed.valueOf()) ? parsed.toISOString() : null;
|
|
225
|
+
}
|
|
226
|
+
function filterDeploymentsForWorkday(deployments, workDay, generatedAt) {
|
|
227
|
+
const startedAt = readDate(workDay, "startedAt", "started_at");
|
|
228
|
+
const endedAt = readDate(workDay, "endedAt", "ended_at") ?? new Date(generatedAt);
|
|
229
|
+
if (!startedAt || !endedAt) {
|
|
230
|
+
return deployments;
|
|
231
|
+
}
|
|
232
|
+
return deployments.filter((deployment) => {
|
|
233
|
+
const relevant = readDate(deployment, "finishedAt", "finished_at") ?? readDate(deployment, "startedAt", "started_at") ?? readDate(deployment, "createdAt", "created_at");
|
|
234
|
+
if (!relevant) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
return relevant.valueOf() >= startedAt.valueOf() && relevant.valueOf() <= endedAt.valueOf();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function fetchRunnerDeployments(config) {
|
|
241
|
+
if (!config.marketBaseUrl || !config.runnerToken) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const url = new URL(`/v1/projects/${encodeURIComponent(config.projectId)}/runner/deployments`, config.marketBaseUrl);
|
|
245
|
+
url.searchParams.set("environment", config.environment);
|
|
246
|
+
const response = await fetch(url, {
|
|
247
|
+
headers: {
|
|
248
|
+
accept: "application/json",
|
|
249
|
+
authorization: `Bearer ${config.runnerToken}`
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const payload = await response.json().catch(() => ({}));
|
|
256
|
+
return asRecords(payload.payload);
|
|
257
|
+
}
|
|
258
|
+
function defaultCreditsForModel(model) {
|
|
259
|
+
switch (model) {
|
|
260
|
+
case "objective":
|
|
261
|
+
return 5;
|
|
262
|
+
case "question":
|
|
263
|
+
return 4;
|
|
264
|
+
case "note":
|
|
265
|
+
case "page":
|
|
266
|
+
return 3;
|
|
267
|
+
case "book":
|
|
268
|
+
case "knowledge":
|
|
269
|
+
return 2;
|
|
270
|
+
default:
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function statusWeight(status) {
|
|
275
|
+
switch (status.toLowerCase()) {
|
|
276
|
+
case "urgent":
|
|
277
|
+
return 50;
|
|
278
|
+
case "blocked":
|
|
279
|
+
return 45;
|
|
280
|
+
case "active":
|
|
281
|
+
case "in_progress":
|
|
282
|
+
case "open":
|
|
283
|
+
return 35;
|
|
284
|
+
case "ready":
|
|
285
|
+
return 30;
|
|
286
|
+
case "draft":
|
|
287
|
+
return 20;
|
|
288
|
+
case "live":
|
|
289
|
+
return 15;
|
|
290
|
+
case "done":
|
|
291
|
+
case "completed":
|
|
292
|
+
return -25;
|
|
293
|
+
case "archived":
|
|
294
|
+
return -40;
|
|
295
|
+
default:
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function modelWeight(model) {
|
|
300
|
+
switch (model) {
|
|
301
|
+
case "objective":
|
|
302
|
+
return 45;
|
|
303
|
+
case "question":
|
|
304
|
+
return 40;
|
|
305
|
+
case "note":
|
|
306
|
+
return 25;
|
|
307
|
+
case "page":
|
|
308
|
+
return 20;
|
|
309
|
+
case "book":
|
|
310
|
+
return 15;
|
|
311
|
+
case "knowledge":
|
|
312
|
+
return 10;
|
|
313
|
+
default:
|
|
314
|
+
return 5;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function relationWeight(record) {
|
|
318
|
+
const relatedCount = readArray(record, "related_objectives", "relatedObjectives").length + readArray(record, "related_questions", "relatedQuestions").length + readArray(record, "related_books", "relatedBooks").length;
|
|
319
|
+
return relatedCount * 4;
|
|
320
|
+
}
|
|
321
|
+
function stalenessWeight(updatedAt, now) {
|
|
322
|
+
if (!updatedAt) {
|
|
323
|
+
return 8;
|
|
324
|
+
}
|
|
325
|
+
const ageDays = Math.max(0, Math.floor((now.valueOf() - updatedAt.valueOf()) / (24 * 60 * 60 * 1e3)));
|
|
326
|
+
if (ageDays >= 90) return 18;
|
|
327
|
+
if (ageDays >= 30) return 12;
|
|
328
|
+
if (ageDays >= 7) return 6;
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
function resolveEstimatedCredits(model, policy, override) {
|
|
332
|
+
const overrideCredits = readNumber(override ?? {}, "estimatedCredits", "estimated_credits");
|
|
333
|
+
if (overrideCredits && overrideCredits > 0) {
|
|
334
|
+
return overrideCredits;
|
|
335
|
+
}
|
|
336
|
+
const weighted = policy.creditWeights.find((weight) => weight.taskType === `${model}_review`);
|
|
337
|
+
return weighted?.credits ?? defaultCreditsForModel(model);
|
|
338
|
+
}
|
|
339
|
+
function summarizeWorkWindow(schedule) {
|
|
340
|
+
return schedule.windows.map((window) => ({
|
|
341
|
+
days: window.days,
|
|
342
|
+
startTime: window.startTime,
|
|
343
|
+
endTime: window.endTime
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
function normalizePolicyRecord(projectId, environment, config) {
|
|
347
|
+
return {
|
|
348
|
+
projectId,
|
|
349
|
+
environment,
|
|
350
|
+
schedule: config.defaultSchedule,
|
|
351
|
+
enabled: true,
|
|
352
|
+
startCron: envValue("TREESEED_WORKDAY_START_CRON") || "0 9 * * 1-5",
|
|
353
|
+
durationMinutes: integerFromEnv("TREESEED_WORKDAY_DURATION_MINUTES", 480),
|
|
354
|
+
maxRunners: config.autoscale.maxWorkers,
|
|
355
|
+
maxWorkersPerRunner: integerFromEnv("TREESEED_RUNNER_MAX_LOCAL_WORKERS", 4),
|
|
356
|
+
dailyCreditBudget: config.dailyTaskCreditBudget,
|
|
357
|
+
closeoutGraceMinutes: integerFromEnv("TREESEED_WORKDAY_CLOSEOUT_GRACE_MINUTES", 15),
|
|
358
|
+
dailyTaskCreditBudget: config.dailyTaskCreditBudget,
|
|
359
|
+
maxQueuedTasks: config.maxQueuedTasks,
|
|
360
|
+
maxQueuedCredits: config.maxQueuedCredits,
|
|
361
|
+
autoscale: config.autoscale,
|
|
362
|
+
creditWeights: config.creditWeights,
|
|
363
|
+
metadata: {
|
|
364
|
+
managedBy: "manager",
|
|
365
|
+
mode: config.mode
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function resolveManagerServiceConfig() {
|
|
370
|
+
const shared = resolveManagerConfig();
|
|
371
|
+
const environment = envValue("TREESEED_DEPLOY_ENVIRONMENT") || (process.env.NODE_ENV === "production" ? "prod" : "local");
|
|
372
|
+
const projectId = envValue("TREESEED_PROJECT_ID") || shared.projectId;
|
|
373
|
+
const teamId = envValue("TREESEED_HOSTING_TEAM_ID") || envValue("TREESEED_CONTENT_DEFAULT_TEAM_ID") || projectId;
|
|
374
|
+
const dailyTaskCreditBudget = integerFromEnv(
|
|
375
|
+
"TREESEED_WORKDAY_TASK_CREDIT_BUDGET",
|
|
376
|
+
integerFromEnv("TREESEED_WORKDAY_CAPACITY_BUDGET", shared.defaultCapacityBudget)
|
|
377
|
+
);
|
|
378
|
+
const maxQueuedTasks = integerFromEnv("TREESEED_MANAGER_MAX_QUEUED_TASKS", Math.max(1, Math.min(20, dailyTaskCreditBudget)));
|
|
379
|
+
const maxQueuedCredits = integerFromEnv("TREESEED_MANAGER_MAX_QUEUED_CREDITS", Math.max(1, Math.min(dailyTaskCreditBudget, maxQueuedTasks * 4)));
|
|
380
|
+
return {
|
|
381
|
+
...shared,
|
|
382
|
+
mode: envValue("TREESEED_MANAGER_MODE") || (process.env.CI ? "reconcile" : "loop"),
|
|
383
|
+
managerId: envValue("TREESEED_MANAGER_ID") || `manager-${process.pid}`,
|
|
384
|
+
marketBaseUrl: envValue("TREESEED_MARKET_API_BASE_URL") || envValue("TREESEED_API_BASE_URL"),
|
|
385
|
+
runnerToken: envValue("TREESEED_PROJECT_RUNNER_TOKEN"),
|
|
386
|
+
projectId,
|
|
387
|
+
teamId,
|
|
388
|
+
environment,
|
|
389
|
+
poolName: envValue("TREESEED_AGENT_POOL_NAME") || `${projectId}-${environment}`,
|
|
390
|
+
serviceBaseUrl: envValue("TREESEED_MANAGER_BASE_URL") || null,
|
|
391
|
+
pollIntervalMs: integerFromEnv("TREESEED_MANAGER_POLL_INTERVAL_MS", 15e3),
|
|
392
|
+
dailyTaskCreditBudget,
|
|
393
|
+
maxQueuedTasks,
|
|
394
|
+
maxQueuedCredits,
|
|
395
|
+
priorityModels: parsePriorityModels(),
|
|
396
|
+
priorityLimitPerModel: integerFromEnv("TREESEED_MANAGER_PRIORITY_LIMIT_PER_MODEL", 50),
|
|
397
|
+
graphInvalidated: booleanFromEnv("TREESEED_MANAGER_GRAPH_INVALIDATED"),
|
|
398
|
+
defaultSchedule: resolveScheduleFromEnv(),
|
|
399
|
+
scalerKind: envValue("TREESEED_WORKER_POOL_SCALER") || "" || null,
|
|
400
|
+
creditWeights: parseJson(envValue("TREESEED_TASK_CREDIT_WEIGHTS_JSON"), []),
|
|
401
|
+
autoscale: {
|
|
402
|
+
minWorkers: integerFromEnv("TREESEED_AGENT_POOL_MIN_WORKERS", 0),
|
|
403
|
+
maxWorkers: integerFromEnv("TREESEED_AGENT_POOL_MAX_WORKERS", 1),
|
|
404
|
+
targetQueueDepth: Math.max(1, integerFromEnv("TREESEED_AGENT_POOL_TARGET_QUEUE_DEPTH", 1)),
|
|
405
|
+
cooldownSeconds: Math.max(0, integerFromEnv("TREESEED_AGENT_POOL_COOLDOWN_SECONDS", 60))
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function resolveReporter(reporter) {
|
|
410
|
+
return reporter ?? createControlPlaneReporter();
|
|
411
|
+
}
|
|
412
|
+
function resolveScaler(config, scaler) {
|
|
413
|
+
return scaler ?? createWorkerPoolScaler(config.scalerKind);
|
|
414
|
+
}
|
|
415
|
+
async function getActiveWorkDay(sdk, projectId) {
|
|
416
|
+
const workDays = await sdk.search({
|
|
417
|
+
model: "work_day",
|
|
418
|
+
limit: 10,
|
|
419
|
+
filters: [
|
|
420
|
+
{ field: "project_id", op: "eq", value: projectId },
|
|
421
|
+
{ field: "state", op: "eq", value: "active" }
|
|
422
|
+
],
|
|
423
|
+
sort: [{ field: "updated_at", direction: "desc" }]
|
|
424
|
+
});
|
|
425
|
+
return asRecords(workDays.payload)[0] ?? null;
|
|
426
|
+
}
|
|
427
|
+
async function ensureWorkPolicy(sdk, config) {
|
|
428
|
+
const existing = await sdk.getWorkPolicy(config.projectId, config.environment);
|
|
429
|
+
if (existing.payload) {
|
|
430
|
+
return existing.payload;
|
|
431
|
+
}
|
|
432
|
+
const created = await sdk.upsertWorkPolicy(normalizePolicyRecord(config.projectId, config.environment, config));
|
|
433
|
+
return created.payload;
|
|
434
|
+
}
|
|
435
|
+
async function loadPriorityInputs(sdk, config) {
|
|
436
|
+
const [overridesEnvelope, ...contentEnvelopes] = await Promise.all([
|
|
437
|
+
sdk.listPriorityOverrides(config.projectId),
|
|
438
|
+
...config.priorityModels.map((model) => sdk.search({
|
|
439
|
+
model,
|
|
440
|
+
limit: config.priorityLimitPerModel,
|
|
441
|
+
sort: [{ field: "updated_at", direction: "desc" }]
|
|
442
|
+
}).catch(() => ({ payload: [] })))
|
|
443
|
+
]);
|
|
444
|
+
const overrides = asRecords(overridesEnvelope.payload).reduce((map, entry) => {
|
|
445
|
+
const model = readString(entry, "model");
|
|
446
|
+
const subjectId = readString(entry, "subjectId", "subject_id");
|
|
447
|
+
if (model && subjectId) {
|
|
448
|
+
map.set(`${model}:${subjectId}`, entry);
|
|
449
|
+
}
|
|
450
|
+
return map;
|
|
451
|
+
}, /* @__PURE__ */ new Map());
|
|
452
|
+
const records = contentEnvelopes.flatMap((envelope, index) => {
|
|
453
|
+
const model = config.priorityModels[index];
|
|
454
|
+
return asRecords(envelope.payload).map((entry) => ({ model, entry }));
|
|
455
|
+
});
|
|
456
|
+
return { overrides, records };
|
|
457
|
+
}
|
|
458
|
+
async function buildPrioritySnapshot(sdk, config, policy, now, workDayId) {
|
|
459
|
+
const { overrides, records } = await loadPriorityInputs(sdk, config);
|
|
460
|
+
const items = records.map(({ model, entry }) => {
|
|
461
|
+
const id = readString(entry, "id", "slug");
|
|
462
|
+
const slug = readString(entry, "slug") || null;
|
|
463
|
+
const title = readString(entry, "title", "name") || null;
|
|
464
|
+
const status = readString(entry, "status", "runtime_status", "runtimeStatus");
|
|
465
|
+
const updatedAt = readDate(entry, "updated_at", "updatedAt", "updated", "date");
|
|
466
|
+
const override = overrides.get(`${model}:${id}`);
|
|
467
|
+
const overridePriority = readNumber(override ?? {}, "priority") ?? 0;
|
|
468
|
+
const reasons = [
|
|
469
|
+
overridePriority > 0 ? `override:${overridePriority}` : null,
|
|
470
|
+
status ? `status:${status}` : null,
|
|
471
|
+
relationWeight(entry) > 0 ? "linked_work" : null,
|
|
472
|
+
updatedAt ? `updated:${updatedAt.toISOString()}` : "updated:unknown"
|
|
473
|
+
].filter((value) => Boolean(value));
|
|
474
|
+
return {
|
|
475
|
+
model,
|
|
476
|
+
id,
|
|
477
|
+
slug,
|
|
478
|
+
title,
|
|
479
|
+
priority: modelWeight(model) + statusWeight(status) + relationWeight(entry) + stalenessWeight(updatedAt, now) + overridePriority,
|
|
480
|
+
estimatedCredits: resolveEstimatedCredits(model, policy, override),
|
|
481
|
+
reasons,
|
|
482
|
+
metadata: {
|
|
483
|
+
status: status || null,
|
|
484
|
+
updatedAt: updatedAt?.toISOString() ?? null,
|
|
485
|
+
overrideId: override ? readString(override, "id") : null
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}).filter((item) => item.id).sort((left, right) => right.priority - left.priority || left.model.localeCompare(right.model) || left.id.localeCompare(right.id));
|
|
489
|
+
const snapshot = await sdk.createPrioritySnapshot({
|
|
490
|
+
projectId: config.projectId,
|
|
491
|
+
workDayId: workDayId ?? null,
|
|
492
|
+
items,
|
|
493
|
+
metadata: {
|
|
494
|
+
models: config.priorityModels,
|
|
495
|
+
schedule: summarizeWorkWindow(policy.schedule)
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
return snapshot.payload;
|
|
499
|
+
}
|
|
500
|
+
async function openWorkday(sdk, config, policy, now, reporter) {
|
|
501
|
+
const capacityPlan = await reporter?.getProjectCapacityPlan(config.environment).catch(() => null) ?? null;
|
|
502
|
+
const capacityEnvelope = await reserveWorkdayCapacity({
|
|
503
|
+
config,
|
|
504
|
+
policy,
|
|
505
|
+
capacityPlan,
|
|
506
|
+
reporter,
|
|
507
|
+
now
|
|
508
|
+
});
|
|
509
|
+
if (capacityPlan && capacityPlan.grants.length > 0 && !capacityEnvelope) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
const created = await sdk.startWorkDay({
|
|
513
|
+
projectId: config.projectId,
|
|
514
|
+
capacityBudget: policy.dailyTaskCreditBudget,
|
|
515
|
+
graphVersion: null,
|
|
516
|
+
summary: {
|
|
517
|
+
openedAt: now.toISOString(),
|
|
518
|
+
environment: config.environment,
|
|
519
|
+
graphRefresh: { state: "queued" },
|
|
520
|
+
capacityPlan: capacityPlan ? summarizeCapacityPlan(capacityPlan) : null,
|
|
521
|
+
capacityEnvelope
|
|
522
|
+
},
|
|
523
|
+
actor: "manager"
|
|
524
|
+
});
|
|
525
|
+
if (created.payload) {
|
|
526
|
+
await seedGraphRefreshTask(sdk, {
|
|
527
|
+
workDayId: String(created.payload.id),
|
|
528
|
+
projectId: config.projectId,
|
|
529
|
+
actor: "manager"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return created.payload;
|
|
533
|
+
}
|
|
534
|
+
async function reserveWorkdayCapacity(options) {
|
|
535
|
+
if (!options.capacityPlan || options.capacityPlan.grants.length === 0 || options.capacityPlan.lanes.length === 0) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const activeGrants = options.capacityPlan.grants.filter((grant2) => grant2.state === "active");
|
|
539
|
+
const candidates = activeGrants.flatMap((grant2) => {
|
|
540
|
+
const lane = grant2.laneId ? options.capacityPlan?.lanes.find((entry) => entry.id === grant2.laneId) : options.capacityPlan?.lanes.find((entry) => entry.capacityProviderId === grant2.capacityProviderId);
|
|
541
|
+
if (!lane) return [];
|
|
542
|
+
return [{
|
|
543
|
+
lane,
|
|
544
|
+
grant: grant2,
|
|
545
|
+
remainingCredits: grant2.dailyCreditLimit ?? options.capacityPlan?.remaining.dailyCredits ?? null,
|
|
546
|
+
taskKind: "workday"
|
|
547
|
+
}];
|
|
548
|
+
});
|
|
549
|
+
const selected = selectBestCapacityLane(candidates).selected;
|
|
550
|
+
if (!selected || !options.reporter?.enabled) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const grant = activeGrants.find((entry) => entry.laneId === selected.laneId || entry.capacityProviderId === selected.capacityProviderId);
|
|
554
|
+
const estimate = reserveCreditsForEstimate({
|
|
555
|
+
taskKind: "workday",
|
|
556
|
+
confidence: "medium",
|
|
557
|
+
estimatedCreditsP50: options.policy.dailyTaskCreditBudget,
|
|
558
|
+
estimatedCreditsP90: options.policy.dailyTaskCreditBudget
|
|
559
|
+
});
|
|
560
|
+
const reservation = await options.reporter.createCapacityReservation({
|
|
561
|
+
capacityProviderId: selected.capacityProviderId,
|
|
562
|
+
laneId: selected.laneId,
|
|
563
|
+
teamId: options.capacityPlan.teamId,
|
|
564
|
+
projectId: options.config.projectId,
|
|
565
|
+
workDayId: null,
|
|
566
|
+
taskId: null,
|
|
567
|
+
reservedCredits: estimate.reservedCredits,
|
|
568
|
+
metadata: {
|
|
569
|
+
environment: options.config.environment,
|
|
570
|
+
grantId: grant?.id ?? null,
|
|
571
|
+
createdBy: "manager.openWorkday",
|
|
572
|
+
createdAt: options.now.toISOString()
|
|
573
|
+
}
|
|
574
|
+
}).catch(() => null);
|
|
575
|
+
if (!reservation) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
await options.reporter.reportCapacityRoutingDecision({
|
|
579
|
+
projectId: options.config.projectId,
|
|
580
|
+
selectedProviderId: selected.capacityProviderId,
|
|
581
|
+
selectedLaneId: selected.laneId,
|
|
582
|
+
decision: "workday_capacity_reserved",
|
|
583
|
+
reason: "Selected eligible capacity lane for workday reservation.",
|
|
584
|
+
scores: { selected },
|
|
585
|
+
candidates: candidates.map((candidate) => ({
|
|
586
|
+
laneId: candidate.lane.id,
|
|
587
|
+
capacityProviderId: candidate.lane.capacityProviderId,
|
|
588
|
+
grantId: candidate.grant?.id ?? null
|
|
589
|
+
}))
|
|
590
|
+
}).catch(() => null);
|
|
591
|
+
return {
|
|
592
|
+
providerId: selected.capacityProviderId,
|
|
593
|
+
laneId: selected.laneId,
|
|
594
|
+
reservationIds: [reservation.id],
|
|
595
|
+
maxCredits: estimate.reservedCredits,
|
|
596
|
+
approvalBehavior: "pause_task",
|
|
597
|
+
pausePolicy: {
|
|
598
|
+
onOverrun: "pause_for_approval"
|
|
599
|
+
},
|
|
600
|
+
metadata: {
|
|
601
|
+
grantId: grant?.id ?? null,
|
|
602
|
+
scarcityLevel: options.capacityPlan.lanes.find((lane) => lane.id === selected.laneId)?.scarcityLevel ?? null
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function remainingCredits(workDay, policy) {
|
|
607
|
+
if (!workDay) {
|
|
608
|
+
return policy.dailyTaskCreditBudget;
|
|
609
|
+
}
|
|
610
|
+
const budget = Number(workDay.capacityBudget ?? policy.dailyTaskCreditBudget ?? 0);
|
|
611
|
+
const used = Number(workDay.capacityUsed ?? 0);
|
|
612
|
+
return Math.max(0, budget - used);
|
|
613
|
+
}
|
|
614
|
+
function capacityEnvelopeFromWorkDay(workDay, maxCredits) {
|
|
615
|
+
const summary = parseJsonString(workDay.summaryJson ?? workDay.summary_json, {});
|
|
616
|
+
const envelope = asRecord(summary.capacityEnvelope);
|
|
617
|
+
if (!Object.keys(envelope).length) {
|
|
618
|
+
return maxCredits ? {
|
|
619
|
+
maxCredits,
|
|
620
|
+
approvalBehavior: "pause_task",
|
|
621
|
+
pausePolicy: { onOverrun: "pause_for_approval" }
|
|
622
|
+
} : null;
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
...envelope,
|
|
626
|
+
maxCredits: maxCredits ?? readNumber(envelope, "maxCredits") ?? null,
|
|
627
|
+
metadata: {
|
|
628
|
+
...asRecord(envelope.metadata),
|
|
629
|
+
inheritedFromWorkDay: true
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function chooseAgentId(agentSpecs) {
|
|
634
|
+
const preferred = agentSpecs.find((spec) => {
|
|
635
|
+
const triggers = Array.isArray(spec.triggers) ? spec.triggers : [];
|
|
636
|
+
return triggers.some((trigger) => {
|
|
637
|
+
const type = typeof trigger === "string" ? trigger : readString(asRecord(trigger), "type");
|
|
638
|
+
return type === "startup" || type === "schedule";
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
return readString(preferred ?? agentSpecs[0] ?? {}, "slug");
|
|
642
|
+
}
|
|
643
|
+
async function maybeEnqueueTask(sdk, task) {
|
|
644
|
+
const queue = createQueuePushClient();
|
|
645
|
+
if (!queue) {
|
|
646
|
+
return { queued: false, queueName: null };
|
|
647
|
+
}
|
|
648
|
+
await queue.enqueue({
|
|
649
|
+
message: queueEnvelopeForTask(task),
|
|
650
|
+
delaySeconds: 0
|
|
651
|
+
});
|
|
652
|
+
await sdk.recordTaskProgress({
|
|
653
|
+
id: String(task.id ?? ""),
|
|
654
|
+
state: "queued",
|
|
655
|
+
appendEvent: {
|
|
656
|
+
kind: "queued",
|
|
657
|
+
data: {
|
|
658
|
+
queueName: envValue("TREESEED_QUEUE_ID") || null
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
actor: "manager"
|
|
662
|
+
});
|
|
663
|
+
return { queued: true, queueName: envValue("TREESEED_QUEUE_ID") || null };
|
|
664
|
+
}
|
|
665
|
+
async function topUpQueuedTasks(sdk, config, policy, workDay, snapshot, now) {
|
|
666
|
+
const agentSpecs = await sdk.listAgentSpecs({ enabled: true });
|
|
667
|
+
const agentId = chooseAgentId(asRecords(agentSpecs));
|
|
668
|
+
if (!agentId || !snapshot?.items.length) {
|
|
669
|
+
return {
|
|
670
|
+
createdTasks: [],
|
|
671
|
+
remainingCandidates: 0,
|
|
672
|
+
remainingCredits: remainingCredits(workDay, policy)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const [allTasksEnvelope, queuedMetrics] = await Promise.all([
|
|
676
|
+
sdk.searchTasks({ workDayId: String(workDay.id ?? ""), limit: 1e3 }),
|
|
677
|
+
collectTaskMetrics(sdk, String(workDay.id ?? ""))
|
|
678
|
+
]);
|
|
679
|
+
const existingTasks = asRecords(allTasksEnvelope.payload);
|
|
680
|
+
const existingKeys = new Set(existingTasks.map((task) => readString(task, "idempotencyKey", "idempotency_key")));
|
|
681
|
+
let availableCredits = remainingCredits(workDay, policy);
|
|
682
|
+
let remainingQueuedSlots = Math.max(0, policy.maxQueuedTasks - queuedMetrics.queuedCount);
|
|
683
|
+
let remainingQueuedCredits = Math.max(0, policy.maxQueuedCredits - queuedMetrics.queuedCredits);
|
|
684
|
+
const createdTasks = [];
|
|
685
|
+
for (const item of snapshot.items) {
|
|
686
|
+
if (remainingQueuedSlots <= 0 || availableCredits <= 0 || remainingQueuedCredits <= 0) {
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
const idempotencyKey = `${String(workDay.id ?? "")}:${item.model}:${item.id}`;
|
|
690
|
+
if (existingKeys.has(idempotencyKey)) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const estimatedCredits = Math.max(1, Math.ceil(item.estimatedCredits));
|
|
694
|
+
if (estimatedCredits > availableCredits || estimatedCredits > remainingQueuedCredits) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const created = await sdk.createTask({
|
|
698
|
+
workDayId: String(workDay.id ?? ""),
|
|
699
|
+
agentId,
|
|
700
|
+
type: `${item.model}_review`,
|
|
701
|
+
priority: Math.max(1, Math.round(item.priority)),
|
|
702
|
+
idempotencyKey,
|
|
703
|
+
payload: {
|
|
704
|
+
subject: {
|
|
705
|
+
model: item.model,
|
|
706
|
+
id: item.id,
|
|
707
|
+
slug: item.slug ?? null,
|
|
708
|
+
title: item.title ?? null
|
|
709
|
+
},
|
|
710
|
+
estimatedCredits,
|
|
711
|
+
priority: item.priority,
|
|
712
|
+
reasons: item.reasons,
|
|
713
|
+
capacityEnvelope: capacityEnvelopeFromWorkDay(workDay, estimatedCredits),
|
|
714
|
+
createdAt: now.toISOString()
|
|
715
|
+
},
|
|
716
|
+
graphVersion: typeof workDay.graphVersion === "string" ? workDay.graphVersion : null,
|
|
717
|
+
actor: "manager"
|
|
718
|
+
});
|
|
719
|
+
if (!created.payload) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
await sdk.recordTaskCredits({
|
|
723
|
+
projectId: config.projectId,
|
|
724
|
+
workDayId: String(workDay.id ?? ""),
|
|
725
|
+
taskId: String(created.payload.id ?? ""),
|
|
726
|
+
phase: "seed",
|
|
727
|
+
credits: estimatedCredits,
|
|
728
|
+
metadata: {
|
|
729
|
+
model: item.model,
|
|
730
|
+
subjectId: item.id
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
await maybeEnqueueTask(sdk, created.payload);
|
|
734
|
+
createdTasks.push(created.payload);
|
|
735
|
+
existingKeys.add(idempotencyKey);
|
|
736
|
+
availableCredits -= estimatedCredits;
|
|
737
|
+
remainingQueuedSlots -= 1;
|
|
738
|
+
remainingQueuedCredits -= estimatedCredits;
|
|
739
|
+
}
|
|
740
|
+
const remainingCandidates = snapshot.items.filter((item) => !existingKeys.has(`${String(workDay.id ?? "")}:${item.model}:${item.id}`)).length;
|
|
741
|
+
return {
|
|
742
|
+
createdTasks,
|
|
743
|
+
remainingCandidates,
|
|
744
|
+
remainingCredits: availableCredits
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function parseCursorTimestamp(value) {
|
|
748
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
const timestamp = new Date(value).valueOf();
|
|
752
|
+
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
753
|
+
}
|
|
754
|
+
function triggerPriority(invocation) {
|
|
755
|
+
switch (invocation.kind) {
|
|
756
|
+
case "message":
|
|
757
|
+
return 90;
|
|
758
|
+
case "follow":
|
|
759
|
+
return 80;
|
|
760
|
+
case "startup":
|
|
761
|
+
return 70;
|
|
762
|
+
case "schedule":
|
|
763
|
+
case "manual":
|
|
764
|
+
default:
|
|
765
|
+
return 60;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function triggerTaskIdempotencyKey(workDayId, agent, invocation) {
|
|
769
|
+
if (invocation.kind === "message" && invocation.message?.id) {
|
|
770
|
+
return `${workDayId}:trigger:${agent.slug}:message:${invocation.message.id}`;
|
|
771
|
+
}
|
|
772
|
+
if (invocation.kind === "follow") {
|
|
773
|
+
return `${workDayId}:trigger:${agent.slug}:follow:${followCursorKey(invocation.followModels)}:${invocation.cursorValue ?? "none"}`;
|
|
774
|
+
}
|
|
775
|
+
const triggerKey = readString(
|
|
776
|
+
asRecord(invocation.trigger),
|
|
777
|
+
"name",
|
|
778
|
+
"type"
|
|
779
|
+
) || invocation.kind;
|
|
780
|
+
return `${workDayId}:trigger:${agent.slug}:${invocation.kind}:${triggerKey}`;
|
|
781
|
+
}
|
|
782
|
+
async function materializeAgentTriggerTasks(sdk, workDay, now) {
|
|
783
|
+
const workDayId = String(workDay.id ?? "");
|
|
784
|
+
if (!workDayId) {
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
const [{ specs, diagnostics }, existingTasksEnvelope] = await Promise.all([
|
|
788
|
+
loadActiveAgentSpecs(sdk),
|
|
789
|
+
sdk.searchTasks({ workDayId, limit: 1e3 })
|
|
790
|
+
]);
|
|
791
|
+
const errors = diagnostics.filter((entry) => entry.severity === "error");
|
|
792
|
+
if (errors.length > 0) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Agent spec validation failed: ${errors.map((entry) => `${entry.slug}:${entry.field}:${entry.message}`).join(" | ")}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const existingKeys = new Set(
|
|
798
|
+
asRecords(existingTasksEnvelope.payload).map((task) => readString(task, "idempotencyKey", "idempotency_key"))
|
|
799
|
+
);
|
|
800
|
+
const createdTasks = [];
|
|
801
|
+
for (const agent of [...specs].sort((left, right) => left.slug.localeCompare(right.slug))) {
|
|
802
|
+
const scopedSdk = sdk.scopeForAgent(agent);
|
|
803
|
+
const lastRunAt = parseCursorTimestamp((await sdk.getCursor({
|
|
804
|
+
agentSlug: agent.slug,
|
|
805
|
+
cursorKey: "last_run_at"
|
|
806
|
+
})).payload);
|
|
807
|
+
const runsThisCycle = agent.triggerPolicy?.maxRunsPerCycle ?? 1;
|
|
808
|
+
for (let index = 0; index < runsThisCycle; index += 1) {
|
|
809
|
+
const decision = await resolveTriggerDecision({
|
|
810
|
+
agent,
|
|
811
|
+
mode: "auto",
|
|
812
|
+
isRunning: false,
|
|
813
|
+
lastRunAt,
|
|
814
|
+
sdk: scopedSdk
|
|
815
|
+
});
|
|
816
|
+
if (decision.kind !== "ready" || !decision.invocation) {
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
const invocation = decision.invocation;
|
|
820
|
+
const idempotencyKey = triggerTaskIdempotencyKey(workDayId, agent, invocation);
|
|
821
|
+
if (existingKeys.has(idempotencyKey)) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const created = await sdk.createTask({
|
|
825
|
+
workDayId,
|
|
826
|
+
agentId: agent.slug,
|
|
827
|
+
type: "agent_trigger",
|
|
828
|
+
priority: triggerPriority(invocation),
|
|
829
|
+
idempotencyKey,
|
|
830
|
+
payload: {
|
|
831
|
+
executionKind: "agent_trigger",
|
|
832
|
+
agentSlug: agent.slug,
|
|
833
|
+
invocation,
|
|
834
|
+
capacityEnvelope: capacityEnvelopeFromWorkDay(workDay),
|
|
835
|
+
createdAt: now.toISOString()
|
|
836
|
+
},
|
|
837
|
+
graphVersion: typeof workDay.graphVersion === "string" ? workDay.graphVersion : null,
|
|
838
|
+
actor: "manager"
|
|
839
|
+
});
|
|
840
|
+
if (!created.payload) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
await maybeEnqueueTask(sdk, created.payload);
|
|
844
|
+
createdTasks.push(created.payload);
|
|
845
|
+
existingKeys.add(idempotencyKey);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return createdTasks;
|
|
849
|
+
}
|
|
850
|
+
async function registerHeartbeat(reporter, config, policy, desiredWorkers, metrics) {
|
|
851
|
+
await reporter.registerAgentPoolHeartbeat({
|
|
852
|
+
teamId: config.teamId,
|
|
853
|
+
environment: config.environment,
|
|
854
|
+
poolName: config.poolName,
|
|
855
|
+
managerId: config.managerId,
|
|
856
|
+
serviceName: "manager",
|
|
857
|
+
registrationIdentity: config.managerId,
|
|
858
|
+
serviceBaseUrl: config.serviceBaseUrl,
|
|
859
|
+
autoscale: policy.autoscale,
|
|
860
|
+
desiredWorkers,
|
|
861
|
+
observedQueueDepth: metrics.queuedCount,
|
|
862
|
+
observedActiveLeases: metrics.activeLeases,
|
|
863
|
+
metadata: {
|
|
864
|
+
projectId: config.projectId,
|
|
865
|
+
managerPort: config.port
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
async function buildWorkdaySummary(sdk, config, workDay, policy, currentSnapshot, scaleDecision, scaleResult) {
|
|
870
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
871
|
+
const [tasksEnvelope, creditsEnvelope, deployments] = await Promise.all([
|
|
872
|
+
sdk.searchTasks({ workDayId: String(workDay.id ?? ""), limit: 1e3 }),
|
|
873
|
+
sdk.listTaskCredits(String(workDay.id ?? "")),
|
|
874
|
+
fetchRunnerDeployments(config)
|
|
875
|
+
]);
|
|
876
|
+
const tasks = asRecords(tasksEnvelope.payload);
|
|
877
|
+
const credits = Array.isArray(creditsEnvelope.payload) ? creditsEnvelope.payload : [];
|
|
878
|
+
const taskDetails = await Promise.all(tasks.map(async (task) => {
|
|
879
|
+
const taskId = readString(task, "id");
|
|
880
|
+
const [eventsEnvelope, outputsEnvelope] = await Promise.all([
|
|
881
|
+
sdk.search({
|
|
882
|
+
model: "task_event",
|
|
883
|
+
filters: [{ field: "taskId", op: "eq", value: taskId }],
|
|
884
|
+
limit: 200
|
|
885
|
+
}),
|
|
886
|
+
sdk.search({
|
|
887
|
+
model: "task_output",
|
|
888
|
+
filters: [{ field: "taskId", op: "eq", value: taskId }],
|
|
889
|
+
limit: 200
|
|
890
|
+
})
|
|
891
|
+
]);
|
|
892
|
+
const taskEvents = asRecords(eventsEnvelope.payload);
|
|
893
|
+
const taskOutputs = asRecords(outputsEnvelope.payload);
|
|
894
|
+
const changedFiles2 = /* @__PURE__ */ new Set();
|
|
895
|
+
for (const output of taskOutputs) {
|
|
896
|
+
normalizeChangedFilesFromValue(parseJsonString(output.outputJson ?? output.output_json), changedFiles2);
|
|
897
|
+
}
|
|
898
|
+
const latestEvent = [...taskEvents].sort((left, right) => Number(readNumber(right, "seq") ?? 0) - Number(readNumber(left, "seq") ?? 0))[0];
|
|
899
|
+
return {
|
|
900
|
+
task: {
|
|
901
|
+
id: taskId,
|
|
902
|
+
agentId: readString(task, "agentId", "agent_id") || void 0,
|
|
903
|
+
type: readString(task, "type") || void 0,
|
|
904
|
+
state: readString(task, "state") || void 0,
|
|
905
|
+
priority: readNumber(task, "priority") ?? void 0,
|
|
906
|
+
idempotencyKey: readString(task, "idempotencyKey", "idempotency_key") || void 0,
|
|
907
|
+
createdAt: isoDateOrNull(readString(task, "createdAt", "created_at")),
|
|
908
|
+
startedAt: isoDateOrNull(readString(task, "startedAt", "started_at")),
|
|
909
|
+
completedAt: isoDateOrNull(readString(task, "completedAt", "completed_at")),
|
|
910
|
+
lastErrorCode: readString(task, "lastErrorCode", "last_error_code") || null,
|
|
911
|
+
lastErrorMessage: readString(task, "lastErrorMessage", "last_error_message") || null,
|
|
912
|
+
lastEventKind: latestEvent ? readString(latestEvent, "kind") || null : null,
|
|
913
|
+
outputCount: taskOutputs.length,
|
|
914
|
+
changedFiles: [...changedFiles2]
|
|
915
|
+
},
|
|
916
|
+
changedFiles: changedFiles2
|
|
917
|
+
};
|
|
918
|
+
}));
|
|
919
|
+
const changedFiles = [...taskDetails.reduce((set, detail) => {
|
|
920
|
+
for (const filePath of detail.changedFiles) {
|
|
921
|
+
set.add(filePath);
|
|
922
|
+
}
|
|
923
|
+
return set;
|
|
924
|
+
}, /* @__PURE__ */ new Set())].sort((left, right) => left.localeCompare(right));
|
|
925
|
+
const releases = filterDeploymentsForWorkday(deployments, workDay, generatedAt).map((deployment) => ({
|
|
926
|
+
id: readString(deployment, "id") || void 0,
|
|
927
|
+
deploymentKind: readString(deployment, "deploymentKind", "deployment_kind") || "code",
|
|
928
|
+
status: readString(deployment, "status") || "unknown",
|
|
929
|
+
releaseTag: readString(deployment, "releaseTag", "release_tag") || null,
|
|
930
|
+
commitSha: readString(deployment, "commitSha", "commit_sha") || null,
|
|
931
|
+
sourceRef: readString(deployment, "sourceRef", "source_ref") || null,
|
|
932
|
+
startedAt: isoDateOrNull(readString(deployment, "startedAt", "started_at")),
|
|
933
|
+
finishedAt: isoDateOrNull(readString(deployment, "finishedAt", "finished_at")),
|
|
934
|
+
createdAt: isoDateOrNull(readString(deployment, "createdAt", "created_at"))
|
|
935
|
+
}));
|
|
936
|
+
const budget = Number(workDay.capacityBudget ?? policy.dailyTaskCreditBudget ?? 0);
|
|
937
|
+
const used = Number(workDay.capacityUsed ?? 0);
|
|
938
|
+
return {
|
|
939
|
+
projectId: config.projectId,
|
|
940
|
+
environment: config.environment,
|
|
941
|
+
workDayId: String(workDay.id ?? ""),
|
|
942
|
+
state: String(workDay.state ?? "active"),
|
|
943
|
+
totalTasks: tasks.length,
|
|
944
|
+
completedTasks: tasks.filter((task) => task.state === "completed").length,
|
|
945
|
+
failedTasks: tasks.filter((task) => task.state === "failed").length,
|
|
946
|
+
queuedTasks: tasks.filter((task) => task.state === "queued" || task.state === "pending").length,
|
|
947
|
+
activeTasks: tasks.filter((task) => task.state === "claimed" || task.state === "running").length,
|
|
948
|
+
dailyTaskCreditBudget: budget,
|
|
949
|
+
usedTaskCredits: used,
|
|
950
|
+
remainingTaskCredits: Math.max(0, budget - used),
|
|
951
|
+
creditLedgerEntries: credits.length,
|
|
952
|
+
prioritySnapshotId: currentSnapshot?.id ?? null,
|
|
953
|
+
priorityItemCount: currentSnapshot?.items.length ?? 0,
|
|
954
|
+
priorityItems: currentSnapshot?.items ?? [],
|
|
955
|
+
taskItems: taskDetails.map((detail) => detail.task),
|
|
956
|
+
changedFiles,
|
|
957
|
+
releases,
|
|
958
|
+
scaleDecision,
|
|
959
|
+
scaleResult,
|
|
960
|
+
generatedAt
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async function reportWorkdaySummary(sdk, reporter, config, workDay, policy, currentSnapshot, scaleDecision, scaleResult) {
|
|
964
|
+
const summary = await buildWorkdaySummary(sdk, config, workDay, policy, currentSnapshot, scaleDecision, scaleResult);
|
|
965
|
+
const capacityPlan = await reporter.getProjectCapacityPlan(config.environment).catch(() => null);
|
|
966
|
+
const enrichedSummary = {
|
|
967
|
+
...summary,
|
|
968
|
+
capacity: capacityPlan ? {
|
|
969
|
+
...summarizeCapacityPlan(capacityPlan),
|
|
970
|
+
providerSplit: capacityPlan.activeReservations.filter((reservation) => reservation.workDayId === String(workDay.id ?? "") || reservation.workDayId === null).map((reservation) => ({
|
|
971
|
+
providerId: reservation.capacityProviderId,
|
|
972
|
+
laneId: reservation.laneId,
|
|
973
|
+
state: reservation.state,
|
|
974
|
+
reservedCredits: reservation.reservedCredits,
|
|
975
|
+
consumedCredits: reservation.consumedCredits,
|
|
976
|
+
reservedProviderUnits: reservation.reservedProviderUnits,
|
|
977
|
+
consumedProviderUnits: reservation.consumedProviderUnits,
|
|
978
|
+
reservedUsd: reservation.reservedUsd,
|
|
979
|
+
consumedUsd: reservation.consumedUsd
|
|
980
|
+
}))
|
|
981
|
+
} : null
|
|
982
|
+
};
|
|
983
|
+
const snapshot = writeWorkdayContentSnapshot({
|
|
984
|
+
repoRoot: process.env.TREESEED_AGENT_REPO_ROOT?.trim() || process.cwd(),
|
|
985
|
+
projectId: config.projectId,
|
|
986
|
+
teamId: config.teamId,
|
|
987
|
+
environment: config.environment,
|
|
988
|
+
workDay,
|
|
989
|
+
summary: enrichedSummary,
|
|
990
|
+
prioritySnapshot: currentSnapshot,
|
|
991
|
+
scaleDecision,
|
|
992
|
+
scaleResult,
|
|
993
|
+
tasks: Array.isArray(enrichedSummary.taskItems) ? enrichedSummary.taskItems : [],
|
|
994
|
+
changedFiles: Array.isArray(enrichedSummary.changedFiles) ? enrichedSummary.changedFiles.filter((entry) => typeof entry === "string") : [],
|
|
995
|
+
releases: Array.isArray(enrichedSummary.releases) ? enrichedSummary.releases : [],
|
|
996
|
+
generatedAt: String(enrichedSummary.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString())
|
|
997
|
+
});
|
|
998
|
+
const report = await sdk.createReport({
|
|
999
|
+
workDayId: String(workDay.id ?? ""),
|
|
1000
|
+
kind: "workday_summary",
|
|
1001
|
+
body: {
|
|
1002
|
+
...enrichedSummary,
|
|
1003
|
+
contentSnapshot: {
|
|
1004
|
+
relativePath: snapshot.relativePath,
|
|
1005
|
+
slug: snapshot.slug,
|
|
1006
|
+
reportVersion: snapshot.reportVersion,
|
|
1007
|
+
title: snapshot.title
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
renderedRef: snapshot.relativePath,
|
|
1011
|
+
sentAt: String(enrichedSummary.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()),
|
|
1012
|
+
actor: "manager"
|
|
1013
|
+
});
|
|
1014
|
+
await reporter.reportWorkdaySummary({
|
|
1015
|
+
environment: config.environment,
|
|
1016
|
+
workDayId: String(workDay.id ?? ""),
|
|
1017
|
+
kind: "workday_summary",
|
|
1018
|
+
state: String(workDay.state ?? "active"),
|
|
1019
|
+
startedAt: readString(workDay, "startedAt", "started_at") || null,
|
|
1020
|
+
endedAt: readString(workDay, "endedAt", "ended_at") || null,
|
|
1021
|
+
summary: enrichedSummary,
|
|
1022
|
+
metadata: {
|
|
1023
|
+
projectId: config.projectId,
|
|
1024
|
+
contentSnapshot: {
|
|
1025
|
+
relativePath: snapshot.relativePath,
|
|
1026
|
+
slug: snapshot.slug,
|
|
1027
|
+
reportVersion: snapshot.reportVersion
|
|
1028
|
+
},
|
|
1029
|
+
reportId: report.payload ? readString(report.payload, "id") || null : null
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
return {
|
|
1033
|
+
...enrichedSummary,
|
|
1034
|
+
contentSnapshot: {
|
|
1035
|
+
relativePath: snapshot.relativePath,
|
|
1036
|
+
slug: snapshot.slug,
|
|
1037
|
+
reportVersion: snapshot.reportVersion,
|
|
1038
|
+
title: snapshot.title
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function shouldCloseWorkday(options) {
|
|
1043
|
+
if (!options.workDay) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
const drained = options.queuedCount === 0 && options.activeLeases === 0;
|
|
1047
|
+
if (!drained) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
return !options.insideWorkWindow || options.remainingCredits <= 0 || options.remainingCandidates <= 0;
|
|
1051
|
+
}
|
|
1052
|
+
async function reconcileManager(options) {
|
|
1053
|
+
const config = options.config ?? resolveManagerServiceConfig();
|
|
1054
|
+
const sdk = options.sdk ?? createServiceSdk();
|
|
1055
|
+
const reporter = await resolveReporter(options.reporter);
|
|
1056
|
+
const scaler = resolveScaler(config, options.scaler);
|
|
1057
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1058
|
+
const policy = await ensureWorkPolicy(sdk, config);
|
|
1059
|
+
if (policy.enabled === false) {
|
|
1060
|
+
return {
|
|
1061
|
+
ok: true,
|
|
1062
|
+
mode: "reconcile",
|
|
1063
|
+
managerId: config.managerId,
|
|
1064
|
+
projectId: config.projectId,
|
|
1065
|
+
environment: config.environment,
|
|
1066
|
+
insideWorkWindow: false,
|
|
1067
|
+
workPolicy: policy,
|
|
1068
|
+
workDay: null,
|
|
1069
|
+
prioritySnapshot: null,
|
|
1070
|
+
seededTasks: [],
|
|
1071
|
+
queuedCount: 0,
|
|
1072
|
+
activeLeases: 0,
|
|
1073
|
+
desiredWorkers: 0,
|
|
1074
|
+
scaleResult: { applied: false, provider: "noop", desiredWorkers: 0, metadata: { reason: "workday_policy_disabled" } },
|
|
1075
|
+
workdaySummary: null
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
const pendingWorkdayRequests = typeof sdk.listWorkdayRequests === "function" ? (await sdk.listWorkdayRequests(config.projectId, config.environment, "pending").catch(() => ({ payload: [] }))).payload ?? [] : [];
|
|
1079
|
+
const manualRunRequested = pendingWorkdayRequests.some((entry) => entry.type === "one_off_run" || entry.type === "retry_open");
|
|
1080
|
+
const earlyCloseRequested = pendingWorkdayRequests.some((entry) => entry.type === "early_close");
|
|
1081
|
+
const insideWorkWindow = !earlyCloseRequested && (manualRunRequested || isWithinWorkWindow(now, policy.schedule));
|
|
1082
|
+
let activeWorkDay = await getActiveWorkDay(sdk, config.projectId);
|
|
1083
|
+
let currentSnapshot = null;
|
|
1084
|
+
if (!activeWorkDay && insideWorkWindow && policy.dailyTaskCreditBudget > 0) {
|
|
1085
|
+
const previewSnapshot = await buildPrioritySnapshot(sdk, config, policy, now, null);
|
|
1086
|
+
if ((previewSnapshot?.items.length ?? 0) > 0) {
|
|
1087
|
+
activeWorkDay = await openWorkday(sdk, config, policy, now, reporter);
|
|
1088
|
+
currentSnapshot = activeWorkDay ? await buildPrioritySnapshot(sdk, config, policy, now, String(activeWorkDay.id ?? "")) : previewSnapshot;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (activeWorkDay && !currentSnapshot) {
|
|
1092
|
+
currentSnapshot = await buildPrioritySnapshot(sdk, config, policy, now, String(activeWorkDay.id ?? ""));
|
|
1093
|
+
}
|
|
1094
|
+
let seedResult = {
|
|
1095
|
+
createdTasks: [],
|
|
1096
|
+
remainingCandidates: currentSnapshot?.items.length ?? 0,
|
|
1097
|
+
remainingCredits: remainingCredits(activeWorkDay, policy)
|
|
1098
|
+
};
|
|
1099
|
+
if (activeWorkDay && insideWorkWindow && seedResult.remainingCredits > 0) {
|
|
1100
|
+
seedResult = await topUpQueuedTasks(sdk, config, policy, activeWorkDay, currentSnapshot, now);
|
|
1101
|
+
}
|
|
1102
|
+
if (activeWorkDay && insideWorkWindow) {
|
|
1103
|
+
const triggerTasks = await materializeAgentTriggerTasks(sdk, activeWorkDay, now);
|
|
1104
|
+
if (triggerTasks.length > 0) {
|
|
1105
|
+
seedResult = {
|
|
1106
|
+
...seedResult,
|
|
1107
|
+
createdTasks: [...seedResult.createdTasks, ...triggerTasks]
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const metrics = await collectTaskMetrics(sdk, activeWorkDay ? String(activeWorkDay.id ?? "") : null);
|
|
1112
|
+
const rawDesiredWorkers = activeWorkDay ? computeDesiredWorkerCount(policy.autoscale, metrics) : 0;
|
|
1113
|
+
const latestScaleDecision = await sdk.getLatestScaleDecision(config.projectId, config.environment, config.poolName);
|
|
1114
|
+
const desiredWorkers = applyInteractiveWakeUpOverride({
|
|
1115
|
+
priorityClass: "background",
|
|
1116
|
+
queuedCount: metrics.queuedCount,
|
|
1117
|
+
currentWorkers: Number(latestScaleDecision.payload?.desiredWorkers ?? 0),
|
|
1118
|
+
desiredWorkers: applyScaleCooldown(policy.autoscale, latestScaleDecision.payload, rawDesiredWorkers, now)
|
|
1119
|
+
});
|
|
1120
|
+
const scaleDecision = {
|
|
1121
|
+
projectId: config.projectId,
|
|
1122
|
+
environment: config.environment,
|
|
1123
|
+
poolName: config.poolName,
|
|
1124
|
+
workDayId: activeWorkDay ? String(activeWorkDay.id ?? "") : null,
|
|
1125
|
+
desiredWorkers,
|
|
1126
|
+
observedQueueDepth: metrics.queuedCount,
|
|
1127
|
+
observedActiveLeases: metrics.activeLeases,
|
|
1128
|
+
reason: desiredWorkers !== rawDesiredWorkers ? "cooldown_hold" : "reconcile",
|
|
1129
|
+
metadata: {
|
|
1130
|
+
insideWorkWindow,
|
|
1131
|
+
remainingCredits: seedResult.remainingCredits,
|
|
1132
|
+
seededTaskCount: seedResult.createdTasks.length
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
const recordedScaleDecision = await sdk.recordScaleDecision(scaleDecision);
|
|
1136
|
+
const appliedScaleDecision = recordedScaleDecision.payload ?? scaleDecision;
|
|
1137
|
+
const scaleResult = await scaler.scale(appliedScaleDecision);
|
|
1138
|
+
await registerHeartbeat(reporter, config, policy, desiredWorkers, metrics);
|
|
1139
|
+
await reporter.reportScaleDecision({
|
|
1140
|
+
environment: config.environment,
|
|
1141
|
+
poolName: config.poolName,
|
|
1142
|
+
workDayId: activeWorkDay ? String(activeWorkDay.id ?? "") : null,
|
|
1143
|
+
desiredWorkers,
|
|
1144
|
+
observedQueueDepth: metrics.queuedCount,
|
|
1145
|
+
observedActiveLeases: metrics.activeLeases,
|
|
1146
|
+
reason: appliedScaleDecision.reason,
|
|
1147
|
+
metadata: {
|
|
1148
|
+
...appliedScaleDecision.metadata,
|
|
1149
|
+
scaleResult
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
let closedWorkDay = null;
|
|
1153
|
+
let workdaySummary = null;
|
|
1154
|
+
if (shouldCloseWorkday({
|
|
1155
|
+
insideWorkWindow,
|
|
1156
|
+
workDay: activeWorkDay,
|
|
1157
|
+
remainingCredits: seedResult.remainingCredits,
|
|
1158
|
+
queuedCount: metrics.queuedCount,
|
|
1159
|
+
activeLeases: metrics.activeLeases,
|
|
1160
|
+
remainingCandidates: seedResult.remainingCandidates
|
|
1161
|
+
})) {
|
|
1162
|
+
if (activeWorkDay) {
|
|
1163
|
+
workdaySummary = await reportWorkdaySummary(
|
|
1164
|
+
sdk,
|
|
1165
|
+
reporter,
|
|
1166
|
+
config,
|
|
1167
|
+
activeWorkDay,
|
|
1168
|
+
policy,
|
|
1169
|
+
currentSnapshot,
|
|
1170
|
+
appliedScaleDecision,
|
|
1171
|
+
scaleResult
|
|
1172
|
+
);
|
|
1173
|
+
const closed = await sdk.closeWorkDay({
|
|
1174
|
+
id: String(activeWorkDay.id ?? ""),
|
|
1175
|
+
state: "completed",
|
|
1176
|
+
summary: workdaySummary,
|
|
1177
|
+
actor: "manager"
|
|
1178
|
+
});
|
|
1179
|
+
closedWorkDay = closed.payload ?? activeWorkDay;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
ok: true,
|
|
1184
|
+
mode: "reconcile",
|
|
1185
|
+
managerId: config.managerId,
|
|
1186
|
+
projectId: config.projectId,
|
|
1187
|
+
environment: config.environment,
|
|
1188
|
+
insideWorkWindow,
|
|
1189
|
+
workPolicy: policy,
|
|
1190
|
+
workDay: closedWorkDay ?? activeWorkDay,
|
|
1191
|
+
prioritySnapshot: currentSnapshot,
|
|
1192
|
+
seededTasks: seedResult.createdTasks,
|
|
1193
|
+
queuedCount: metrics.queuedCount,
|
|
1194
|
+
activeLeases: metrics.activeLeases,
|
|
1195
|
+
desiredWorkers,
|
|
1196
|
+
scaleResult,
|
|
1197
|
+
workdaySummary
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function runOpenWorkday(options) {
|
|
1201
|
+
const config = options.config ?? resolveManagerServiceConfig();
|
|
1202
|
+
const sdk = options.sdk ?? createServiceSdk();
|
|
1203
|
+
const reporter = await resolveReporter(options.reporter);
|
|
1204
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1205
|
+
const policy = await ensureWorkPolicy(sdk, config);
|
|
1206
|
+
const active = await getActiveWorkDay(sdk, config.projectId);
|
|
1207
|
+
if (active) {
|
|
1208
|
+
return { ok: true, created: false, workDay: active };
|
|
1209
|
+
}
|
|
1210
|
+
if (policy.enabled === false) {
|
|
1211
|
+
return { ok: true, created: false, skipped: true, reason: "workday_policy_disabled" };
|
|
1212
|
+
}
|
|
1213
|
+
if (!isWithinWorkWindow(now, policy.schedule)) {
|
|
1214
|
+
return { ok: true, created: false, skipped: true, reason: "outside_work_window" };
|
|
1215
|
+
}
|
|
1216
|
+
const workDay = await openWorkday(sdk, config, policy, now, reporter);
|
|
1217
|
+
const prioritySnapshot = workDay ? await buildPrioritySnapshot(sdk, config, policy, now, String(workDay.id ?? "")) : null;
|
|
1218
|
+
return { ok: true, created: Boolean(workDay), workDay, prioritySnapshot };
|
|
1219
|
+
}
|
|
1220
|
+
async function runCloseWorkday(options) {
|
|
1221
|
+
const config = options.config ?? resolveManagerServiceConfig();
|
|
1222
|
+
const sdk = options.sdk ?? createServiceSdk();
|
|
1223
|
+
const reporter = await resolveReporter(options.reporter);
|
|
1224
|
+
const scaler = resolveScaler(config, options.scaler);
|
|
1225
|
+
const policy = await ensureWorkPolicy(sdk, config);
|
|
1226
|
+
const activeWorkDay = await getActiveWorkDay(sdk, config.projectId);
|
|
1227
|
+
if (!activeWorkDay) {
|
|
1228
|
+
return { ok: true, skipped: true, reason: "no_active_workday" };
|
|
1229
|
+
}
|
|
1230
|
+
const decision = {
|
|
1231
|
+
projectId: config.projectId,
|
|
1232
|
+
environment: config.environment,
|
|
1233
|
+
poolName: config.poolName,
|
|
1234
|
+
workDayId: String(activeWorkDay.id ?? ""),
|
|
1235
|
+
desiredWorkers: 0,
|
|
1236
|
+
observedQueueDepth: 0,
|
|
1237
|
+
observedActiveLeases: 0,
|
|
1238
|
+
reason: "close_workday",
|
|
1239
|
+
metadata: {
|
|
1240
|
+
requestedBy: "manager"
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
const recorded = await sdk.recordScaleDecision(decision);
|
|
1244
|
+
const scale = await scaler.scale(recorded.payload ?? decision);
|
|
1245
|
+
const latestSnapshot = await sdk.getLatestPrioritySnapshot(config.projectId, String(activeWorkDay.id ?? ""));
|
|
1246
|
+
const summary = await reportWorkdaySummary(
|
|
1247
|
+
sdk,
|
|
1248
|
+
reporter,
|
|
1249
|
+
config,
|
|
1250
|
+
activeWorkDay,
|
|
1251
|
+
policy,
|
|
1252
|
+
latestSnapshot.payload,
|
|
1253
|
+
recorded.payload ?? decision,
|
|
1254
|
+
scale
|
|
1255
|
+
);
|
|
1256
|
+
const closed = await sdk.closeWorkDay({
|
|
1257
|
+
id: String(activeWorkDay.id ?? ""),
|
|
1258
|
+
state: "completed",
|
|
1259
|
+
summary,
|
|
1260
|
+
actor: "manager"
|
|
1261
|
+
});
|
|
1262
|
+
return { ok: true, workDay: closed.payload, summary, scale };
|
|
1263
|
+
}
|
|
1264
|
+
async function runReportWorkday(options) {
|
|
1265
|
+
const config = options.config ?? resolveManagerServiceConfig();
|
|
1266
|
+
const sdk = options.sdk ?? createServiceSdk();
|
|
1267
|
+
const reporter = await resolveReporter(options.reporter);
|
|
1268
|
+
const policy = await ensureWorkPolicy(sdk, config);
|
|
1269
|
+
const activeWorkDay = await getActiveWorkDay(sdk, config.projectId);
|
|
1270
|
+
if (!activeWorkDay) {
|
|
1271
|
+
return { ok: true, skipped: true, reason: "no_active_workday" };
|
|
1272
|
+
}
|
|
1273
|
+
const latestScaleDecision = await sdk.getLatestScaleDecision(config.projectId, config.environment, config.poolName);
|
|
1274
|
+
const latestSnapshot = await sdk.getLatestPrioritySnapshot(config.projectId, String(activeWorkDay.id ?? ""));
|
|
1275
|
+
const summary = await reportWorkdaySummary(
|
|
1276
|
+
sdk,
|
|
1277
|
+
reporter,
|
|
1278
|
+
config,
|
|
1279
|
+
activeWorkDay,
|
|
1280
|
+
policy,
|
|
1281
|
+
latestSnapshot.payload,
|
|
1282
|
+
latestScaleDecision.payload ?? {
|
|
1283
|
+
projectId: config.projectId,
|
|
1284
|
+
environment: config.environment,
|
|
1285
|
+
poolName: config.poolName,
|
|
1286
|
+
workDayId: String(activeWorkDay.id ?? ""),
|
|
1287
|
+
desiredWorkers: 0,
|
|
1288
|
+
observedQueueDepth: 0,
|
|
1289
|
+
observedActiveLeases: 0,
|
|
1290
|
+
reason: "report_workday",
|
|
1291
|
+
metadata: {},
|
|
1292
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
applied: false,
|
|
1296
|
+
provider: "noop",
|
|
1297
|
+
desiredWorkers: Number(latestScaleDecision.payload?.desiredWorkers ?? 0),
|
|
1298
|
+
metadata: {
|
|
1299
|
+
reason: "report_only"
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
);
|
|
1303
|
+
return { ok: true, workDayId: activeWorkDay.id, summary };
|
|
1304
|
+
}
|
|
1305
|
+
async function runManagerAction(options = {}) {
|
|
1306
|
+
const mode = options.mode ?? options.config?.mode ?? resolveManagerServiceConfig().mode;
|
|
1307
|
+
switch (mode) {
|
|
1308
|
+
case "open-workday":
|
|
1309
|
+
return runOpenWorkday(options);
|
|
1310
|
+
case "close-workday":
|
|
1311
|
+
return runCloseWorkday(options);
|
|
1312
|
+
case "report-workday":
|
|
1313
|
+
return runReportWorkday(options);
|
|
1314
|
+
case "reconcile":
|
|
1315
|
+
return reconcileManager(options);
|
|
1316
|
+
case "loop":
|
|
1317
|
+
default:
|
|
1318
|
+
return reconcileManager(options);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async function runManagerCycle(options = {}) {
|
|
1322
|
+
return reconcileManager(options);
|
|
1323
|
+
}
|
|
1324
|
+
async function startManagerLoop(options = {}) {
|
|
1325
|
+
const config = options.config ?? resolveManagerServiceConfig();
|
|
1326
|
+
for (; ; ) {
|
|
1327
|
+
try {
|
|
1328
|
+
await reconcileManager({
|
|
1329
|
+
...options,
|
|
1330
|
+
config
|
|
1331
|
+
});
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
1334
|
+
`);
|
|
1335
|
+
}
|
|
1336
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, config.pollIntervalMs));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function readCliMode() {
|
|
1340
|
+
const args = process.argv.slice(2);
|
|
1341
|
+
const index = args.indexOf("--mode");
|
|
1342
|
+
if (index >= 0) {
|
|
1343
|
+
return args[index + 1];
|
|
1344
|
+
}
|
|
1345
|
+
return void 0;
|
|
1346
|
+
}
|
|
1347
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1348
|
+
const entryFile = process.argv[1] ?? "";
|
|
1349
|
+
if (entryFile === currentFile) {
|
|
1350
|
+
const mode = readCliMode() ?? resolveManagerServiceConfig().mode;
|
|
1351
|
+
if (mode === "loop") {
|
|
1352
|
+
await startManagerLoop({
|
|
1353
|
+
config: {
|
|
1354
|
+
...resolveManagerServiceConfig(),
|
|
1355
|
+
mode
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
} else {
|
|
1359
|
+
process.stdout.write(`${JSON.stringify(await runManagerAction({ mode }), null, 2)}
|
|
1360
|
+
`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
export {
|
|
1364
|
+
resolveManagerServiceConfig,
|
|
1365
|
+
runManagerAction,
|
|
1366
|
+
runManagerCycle,
|
|
1367
|
+
startManagerLoop
|
|
1368
|
+
};
|