@treeseed/core 0.6.37 → 0.6.39
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/README.md +3 -3
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +2 -0
- package/dist/agents/spec-normalizer.js +71 -1
- package/dist/api/agent-routes.js +1 -11
- package/dist/scripts/build-dist.js +15 -0
- package/dist/services/common.d.ts +13 -0
- package/dist/services/common.js +33 -5
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +2 -0
- package/dist/services/manager.d.ts +24 -4
- package/dist/services/manager.js +184 -18
- package/dist/services/workday-manager.d.ts +279 -0
- package/dist/services/workday-manager.js +163 -0
- package/dist/services/workday-report.d.ts +23 -3
- package/dist/services/workday-start.d.ts +23 -3
- package/dist/services/worker-pool-scaler.d.ts +3 -3
- package/dist/services/worker-pool-scaler.js +69 -51
- package/dist/services/worker.d.ts +6 -0
- package/dist/services/worker.js +247 -13
- package/package.json +12 -6
package/dist/services/worker.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { AgentKernel } from "../agents/kernel/agent-kernel.js";
|
|
6
|
+
import { createControlPlaneReporter } from "@treeseed/sdk";
|
|
4
7
|
import { buildTaskContext, createQueueClient, createServiceSdk, resolveServiceRepoRoot, resolveWorkerConfig } from "./common.js";
|
|
5
8
|
function parseTaskPayload(task) {
|
|
6
9
|
const raw = typeof task?.payloadJson === "string" ? task.payloadJson : "{}";
|
|
@@ -10,11 +13,114 @@ function parseTaskPayload(task) {
|
|
|
10
13
|
return {};
|
|
11
14
|
}
|
|
12
15
|
}
|
|
16
|
+
function asRecord(value) {
|
|
17
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
18
|
+
}
|
|
19
|
+
function readCapacityEnvelope(payload) {
|
|
20
|
+
const envelope = asRecord(payload.capacityEnvelope);
|
|
21
|
+
return Object.keys(envelope).length > 0 ? envelope : null;
|
|
22
|
+
}
|
|
23
|
+
function runnerRepositoryPath(volumeRoot, repositoryId, taskId) {
|
|
24
|
+
const repositoryRoot = join(volumeRoot, "repositories", repositoryId);
|
|
25
|
+
return {
|
|
26
|
+
repositoryRoot,
|
|
27
|
+
bareGit: join(repositoryRoot, "bare.git"),
|
|
28
|
+
worktree: join(repositoryRoot, "worktrees", taskId)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
class WorkerPausedForApproval extends Error {
|
|
32
|
+
constructor(request) {
|
|
33
|
+
super(String(request.summary ?? request.title ?? "Task paused for approval."));
|
|
34
|
+
this.request = request;
|
|
35
|
+
}
|
|
36
|
+
request;
|
|
37
|
+
}
|
|
13
38
|
async function executeQueuedTask(options) {
|
|
14
39
|
const context = await buildTaskContext(options.sdk, options.taskId);
|
|
15
40
|
const task = context.task;
|
|
16
41
|
const payload = parseTaskPayload(task);
|
|
42
|
+
const capacityEnvelope = readCapacityEnvelope(payload);
|
|
43
|
+
const explicitApproval = asRecord(payload.approvalRequest);
|
|
44
|
+
if (Object.keys(explicitApproval).length > 0 || capacityEnvelope?.maxCredits === 0) {
|
|
45
|
+
throw new WorkerPausedForApproval({
|
|
46
|
+
kind: String(explicitApproval.kind ?? "capacity_boundary"),
|
|
47
|
+
title: String(explicitApproval.title ?? "Task paused for approval"),
|
|
48
|
+
summary: String(explicitApproval.summary ?? "The task reached a boundary outside its approved execution envelope."),
|
|
49
|
+
severity: explicitApproval.severity ?? "medium",
|
|
50
|
+
workDayId: task?.workDayId ?? task?.work_day_id ?? null,
|
|
51
|
+
taskId: options.taskId,
|
|
52
|
+
options: Array.isArray(explicitApproval.options) ? explicitApproval.options : [],
|
|
53
|
+
recommendation: asRecord(explicitApproval.recommendation),
|
|
54
|
+
policySnapshot: {
|
|
55
|
+
capacityEnvelope,
|
|
56
|
+
...asRecord(explicitApproval.policySnapshot)
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
17
60
|
const executionKind = typeof payload.executionKind === "string" ? payload.executionKind : null;
|
|
61
|
+
if (String(task?.type ?? task?.taskType ?? "") === "refresh_project_graph") {
|
|
62
|
+
const config = resolveWorkerConfig();
|
|
63
|
+
const projectId = typeof payload.projectId === "string" ? payload.projectId : config.projectId;
|
|
64
|
+
const repositoryId = typeof payload.repositoryId === "string" ? payload.repositoryId : projectId;
|
|
65
|
+
const paths = runnerRepositoryPath(config.volumeRoot, repositoryId, options.taskId);
|
|
66
|
+
await mkdir(paths.bareGit, { recursive: true });
|
|
67
|
+
await mkdir(paths.worktree, { recursive: true });
|
|
68
|
+
const graphRefresh = await options.sdk.refreshGraph();
|
|
69
|
+
const graphVersion = graphRefresh.snapshotRoot;
|
|
70
|
+
await options.sdk.create({
|
|
71
|
+
model: "graph_run",
|
|
72
|
+
data: {
|
|
73
|
+
id: `${options.taskId}:graph`,
|
|
74
|
+
workDayId: String(task?.workDayId ?? task?.work_day_id ?? ""),
|
|
75
|
+
corpusHash: graphVersion,
|
|
76
|
+
graphVersion,
|
|
77
|
+
statsJson: JSON.stringify(graphRefresh),
|
|
78
|
+
snapshotRef: graphVersion
|
|
79
|
+
},
|
|
80
|
+
actor: "worker"
|
|
81
|
+
});
|
|
82
|
+
if (typeof options.sdk.updateWorkDayGraph === "function") {
|
|
83
|
+
await options.sdk.updateWorkDayGraph({
|
|
84
|
+
id: String(task?.workDayId ?? task?.work_day_id ?? ""),
|
|
85
|
+
graphVersion,
|
|
86
|
+
summaryPatch: {
|
|
87
|
+
graphRefresh: {
|
|
88
|
+
state: "completed",
|
|
89
|
+
graphVersion,
|
|
90
|
+
snapshotRef: graphVersion,
|
|
91
|
+
runnerId: config.workerId
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (typeof options.sdk.recordRepositoryClaim === "function") {
|
|
97
|
+
await options.sdk.recordRepositoryClaim({
|
|
98
|
+
projectId,
|
|
99
|
+
repositoryId,
|
|
100
|
+
runnerId: config.workerId,
|
|
101
|
+
runnerServiceName: config.runnerServiceName,
|
|
102
|
+
volumeIdentity: config.volumeIdentity,
|
|
103
|
+
lastSeenCommit: typeof payload.commitSha === "string" ? payload.commitSha : null,
|
|
104
|
+
metadata: {
|
|
105
|
+
bareGit: paths.bareGit,
|
|
106
|
+
worktree: paths.worktree
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
workerId: options.workerId,
|
|
112
|
+
queueAttempt: options.queueAttempt,
|
|
113
|
+
graphVersion,
|
|
114
|
+
snapshotRef: graphVersion,
|
|
115
|
+
repositoryId,
|
|
116
|
+
paths,
|
|
117
|
+
summary: {
|
|
118
|
+
status: "completed",
|
|
119
|
+
workerId: options.workerId,
|
|
120
|
+
summary: `Refreshed project graph ${graphVersion}`
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
18
124
|
if (executionKind === "workflow_dispatch" || executionKind === "sdk_dispatch") {
|
|
19
125
|
const namespace = typeof payload.namespace === "string" ? payload.namespace : "workflow";
|
|
20
126
|
const operation = typeof payload.operation === "string" ? payload.operation : "";
|
|
@@ -57,7 +163,14 @@ async function executeQueuedTask(options) {
|
|
|
57
163
|
status: agentResult.status,
|
|
58
164
|
workerId: options.workerId,
|
|
59
165
|
summary: agentResult.summary
|
|
60
|
-
}
|
|
166
|
+
},
|
|
167
|
+
capacityUsage: capacityEnvelope?.providerId && capacityEnvelope?.laneId ? {
|
|
168
|
+
capacityProviderId: capacityEnvelope.providerId,
|
|
169
|
+
laneId: capacityEnvelope.laneId,
|
|
170
|
+
reservationId: capacityEnvelope.reservationIds?.[0] ?? null,
|
|
171
|
+
credits: Number(payload.estimatedCredits ?? capacityEnvelope.maxCredits ?? 1),
|
|
172
|
+
source: "worker"
|
|
173
|
+
} : null
|
|
61
174
|
};
|
|
62
175
|
}
|
|
63
176
|
async function runWorkerCycle() {
|
|
@@ -65,6 +178,21 @@ async function runWorkerCycle() {
|
|
|
65
178
|
const queue = createQueueClient();
|
|
66
179
|
const config = resolveWorkerConfig();
|
|
67
180
|
const kernel = new AgentKernel(sdk, resolveServiceRepoRoot());
|
|
181
|
+
if (typeof sdk.recordWorkerRunner === "function") {
|
|
182
|
+
await sdk.recordWorkerRunner({
|
|
183
|
+
projectId: config.projectId,
|
|
184
|
+
environment: config.environment,
|
|
185
|
+
runnerId: config.workerId,
|
|
186
|
+
runnerServiceName: config.runnerServiceName,
|
|
187
|
+
volumeIdentity: config.volumeIdentity,
|
|
188
|
+
state: "active",
|
|
189
|
+
maxLocalWorkers: config.maxLocalWorkers,
|
|
190
|
+
activeLocalWorkers: 0,
|
|
191
|
+
metadata: {
|
|
192
|
+
volumeRoot: config.volumeRoot
|
|
193
|
+
}
|
|
194
|
+
}).catch(() => null);
|
|
195
|
+
}
|
|
68
196
|
if (!queue) {
|
|
69
197
|
if (process.env.TREESEED_LOCAL_DEV_MODE?.trim()) {
|
|
70
198
|
return { ok: true, processed: 0, idle: true, reason: "queue_unconfigured" };
|
|
@@ -78,8 +206,9 @@ async function runWorkerCycle() {
|
|
|
78
206
|
if (pulled.messages.length === 0) {
|
|
79
207
|
return { ok: true, processed: 0 };
|
|
80
208
|
}
|
|
81
|
-
|
|
82
|
-
|
|
209
|
+
const maxLocalWorkers = Number.isFinite(Number(config.maxLocalWorkers)) ? Math.max(1, Number(config.maxLocalWorkers)) : 1;
|
|
210
|
+
const selectedMessages = pulled.messages.slice(0, maxLocalWorkers);
|
|
211
|
+
const results = await Promise.all(selectedMessages.map(async (message) => {
|
|
83
212
|
try {
|
|
84
213
|
await sdk.claimTask({
|
|
85
214
|
id: message.body.taskId,
|
|
@@ -97,21 +226,78 @@ async function runWorkerCycle() {
|
|
|
97
226
|
},
|
|
98
227
|
actor: "worker"
|
|
99
228
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
229
|
+
let output;
|
|
230
|
+
try {
|
|
231
|
+
output = await executeQueuedTask({
|
|
232
|
+
sdk,
|
|
233
|
+
kernel,
|
|
234
|
+
taskId: message.body.taskId,
|
|
235
|
+
workerId: config.workerId,
|
|
236
|
+
queueAttempt: message.attempts
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error instanceof WorkerPausedForApproval) {
|
|
240
|
+
const reporter = createControlPlaneReporter();
|
|
241
|
+
const context = await buildTaskContext(sdk, message.body.taskId);
|
|
242
|
+
const task = context.task;
|
|
243
|
+
const projectId = String(process.env.TREESEED_PROJECT_ID ?? "");
|
|
244
|
+
await reporter.createApprovalRequest({
|
|
245
|
+
projectId,
|
|
246
|
+
teamId: String(error.request.teamId ?? process.env.TREESEED_TEAM_ID ?? ""),
|
|
247
|
+
workDayId: typeof error.request.workDayId === "string" ? error.request.workDayId : String(task?.workDayId ?? task?.work_day_id ?? ""),
|
|
248
|
+
taskId: message.body.taskId,
|
|
249
|
+
kind: String(error.request.kind ?? "capacity_boundary"),
|
|
250
|
+
severity: error.request.severity === "high" || error.request.severity === "low" ? error.request.severity : "medium",
|
|
251
|
+
requestedByType: "worker",
|
|
252
|
+
requestedById: config.workerId,
|
|
253
|
+
title: String(error.request.title ?? "Task paused for approval"),
|
|
254
|
+
summary: String(error.request.summary ?? error.message),
|
|
255
|
+
options: Array.isArray(error.request.options) ? error.request.options : [],
|
|
256
|
+
recommendation: asRecord(error.request.recommendation),
|
|
257
|
+
policySnapshot: asRecord(error.request.policySnapshot)
|
|
258
|
+
}).catch(() => null);
|
|
259
|
+
await sdk.recordTaskProgress({
|
|
260
|
+
id: message.body.taskId,
|
|
261
|
+
workerId: config.workerId,
|
|
262
|
+
state: "paused_for_approval",
|
|
263
|
+
appendEvent: {
|
|
264
|
+
kind: "paused_for_approval",
|
|
265
|
+
data: error.request
|
|
266
|
+
},
|
|
267
|
+
actor: "worker"
|
|
268
|
+
});
|
|
269
|
+
await queue.ack([message.leaseId]);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
107
274
|
await sdk.completeTask({
|
|
108
275
|
id: message.body.taskId,
|
|
109
276
|
output,
|
|
110
277
|
summary: output.summary,
|
|
111
278
|
actor: "worker"
|
|
112
279
|
});
|
|
280
|
+
if (output.capacityUsage?.capacityProviderId && output.capacityUsage?.laneId) {
|
|
281
|
+
const reporter = createControlPlaneReporter();
|
|
282
|
+
await reporter.reportCapacityUsage({
|
|
283
|
+
capacityProviderId: output.capacityUsage.capacityProviderId,
|
|
284
|
+
laneId: output.capacityUsage.laneId,
|
|
285
|
+
reservationId: output.capacityUsage.reservationId,
|
|
286
|
+
teamId: String(process.env.TREESEED_TEAM_ID ?? ""),
|
|
287
|
+
projectId: String(process.env.TREESEED_PROJECT_ID ?? ""),
|
|
288
|
+
workDayId: message.body.workDayId,
|
|
289
|
+
taskId: message.body.taskId,
|
|
290
|
+
phase: "consume",
|
|
291
|
+
credits: output.capacityUsage.credits,
|
|
292
|
+
source: "worker",
|
|
293
|
+
metadata: {
|
|
294
|
+
workerId: config.workerId,
|
|
295
|
+
queueAttempt: message.attempts
|
|
296
|
+
}
|
|
297
|
+
}).catch(() => null);
|
|
298
|
+
}
|
|
113
299
|
await queue.ack([message.leaseId]);
|
|
114
|
-
|
|
300
|
+
return 1;
|
|
115
301
|
} catch (error) {
|
|
116
302
|
const retryDelaySeconds = Math.min(300, Math.max(15, message.attempts * 30));
|
|
117
303
|
await sdk.failTask({
|
|
@@ -122,15 +308,62 @@ async function runWorkerCycle() {
|
|
|
122
308
|
actor: "worker"
|
|
123
309
|
}).catch(() => null);
|
|
124
310
|
await queue.retry([{ leaseId: message.leaseId, delaySeconds: retryDelaySeconds }]);
|
|
311
|
+
return 0;
|
|
125
312
|
}
|
|
313
|
+
}));
|
|
314
|
+
return { ok: true, processed: results.reduce((sum, value) => sum + value, 0) };
|
|
315
|
+
}
|
|
316
|
+
function shouldExitWorkerLoopAfterIdle(options) {
|
|
317
|
+
const idleExitMs = Number(options.idleExitMs ?? 0);
|
|
318
|
+
if (!Number.isFinite(idleExitMs) || idleExitMs <= 0) {
|
|
319
|
+
return false;
|
|
126
320
|
}
|
|
127
|
-
|
|
321
|
+
if (options.processed > 0 || options.idleSince === null) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return options.now - options.idleSince >= idleExitMs;
|
|
325
|
+
}
|
|
326
|
+
async function recordWorkerLoopExitState(config) {
|
|
327
|
+
const sdk = createServiceSdk();
|
|
328
|
+
if (typeof sdk.recordWorkerRunner !== "function") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await sdk.recordWorkerRunner({
|
|
332
|
+
projectId: config.projectId,
|
|
333
|
+
environment: config.environment,
|
|
334
|
+
runnerId: config.workerId,
|
|
335
|
+
runnerServiceName: config.runnerServiceName,
|
|
336
|
+
volumeIdentity: config.volumeIdentity,
|
|
337
|
+
state: "sleeping",
|
|
338
|
+
maxLocalWorkers: config.maxLocalWorkers,
|
|
339
|
+
activeLocalWorkers: 0,
|
|
340
|
+
metadata: {
|
|
341
|
+
volumeRoot: config.volumeRoot,
|
|
342
|
+
reason: "idle_exit"
|
|
343
|
+
}
|
|
344
|
+
}).catch(() => null);
|
|
128
345
|
}
|
|
129
346
|
async function startWorkerLoop() {
|
|
130
347
|
const config = resolveWorkerConfig();
|
|
348
|
+
let idleSince = null;
|
|
131
349
|
for (; ; ) {
|
|
132
350
|
try {
|
|
133
|
-
await runWorkerCycle();
|
|
351
|
+
const result = await runWorkerCycle();
|
|
352
|
+
const processed = Number(result.processed ?? 0);
|
|
353
|
+
if (processed > 0) {
|
|
354
|
+
idleSince = null;
|
|
355
|
+
} else {
|
|
356
|
+
idleSince ??= Date.now();
|
|
357
|
+
if (shouldExitWorkerLoopAfterIdle({
|
|
358
|
+
idleExitMs: config.idleExitMs,
|
|
359
|
+
idleSince,
|
|
360
|
+
now: Date.now(),
|
|
361
|
+
processed
|
|
362
|
+
})) {
|
|
363
|
+
await recordWorkerLoopExitState(config);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
134
367
|
} catch (error) {
|
|
135
368
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
136
369
|
`);
|
|
@@ -145,5 +378,6 @@ if (entryFile === currentFile) {
|
|
|
145
378
|
}
|
|
146
379
|
export {
|
|
147
380
|
runWorkerCycle,
|
|
381
|
+
shouldExitWorkerLoopAfterIdle,
|
|
148
382
|
startWorkerLoop
|
|
149
383
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.39",
|
|
4
4
|
"description": "Treeseed integrated platform starter for Astro/Starlight web runtimes and Hono API runtimes.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -42,12 +42,14 @@
|
|
|
42
42
|
"dev": "node ./scripts/run-ts.mjs ./scripts/dev-platform.ts",
|
|
43
43
|
"dev:web": "node ./scripts/run-ts.mjs ./scripts/dev-platform.ts --surface web",
|
|
44
44
|
"dev:api": "node ./scripts/run-ts.mjs ./scripts/dev-platform.ts --surface api",
|
|
45
|
-
"dev:manager": "node ./scripts/run-ts.mjs ./src/services/manager.ts",
|
|
46
|
-
"dev:worker": "node ./scripts/run-ts.mjs ./src/services/worker.ts",
|
|
45
|
+
"dev:workday-manager": "node ./scripts/run-ts.mjs ./src/services/workday-manager.ts",
|
|
46
|
+
"dev:worker-runner": "node ./scripts/run-ts.mjs ./src/services/worker.ts",
|
|
47
|
+
"dev:manager": "npm run dev:workday-manager",
|
|
48
|
+
"dev:worker": "npm run dev:worker-runner",
|
|
47
49
|
"dev:agents": "node ./scripts/run-ts.mjs ./src/services/agents.ts",
|
|
48
50
|
"dev:remote-runner": "node ./scripts/run-ts.mjs ./src/services/remote-runner.ts",
|
|
49
|
-
"dev:workday-start": "
|
|
50
|
-
"dev:workday-report": "
|
|
51
|
+
"dev:workday-start": "npm run dev:workday-manager",
|
|
52
|
+
"dev:workday-report": "npm run dev:workday-manager",
|
|
51
53
|
"dev:watch": "node ./scripts/run-ts.mjs ./scripts/dev-platform.ts --watch",
|
|
52
54
|
"starlight:patch": "node ./scripts/run-ts.mjs ./scripts/patch-starlight-content-path.ts",
|
|
53
55
|
"precheck": "npm run starlight:patch",
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
"@astrojs/sitemap": "3.7.0",
|
|
77
79
|
"@astrojs/starlight": "0.37.6",
|
|
78
80
|
"@tailwindcss/vite": "^4.1.4",
|
|
79
|
-
"@treeseed/sdk": "0.6.
|
|
81
|
+
"@treeseed/sdk": "0.6.40",
|
|
80
82
|
"astro": "^5.6.1",
|
|
81
83
|
"esbuild": "^0.28.0",
|
|
82
84
|
"hono": "^4.8.2",
|
|
@@ -187,6 +189,10 @@
|
|
|
187
189
|
"types": "./dist/services/remote-runner.d.ts",
|
|
188
190
|
"default": "./dist/services/remote-runner.js"
|
|
189
191
|
},
|
|
192
|
+
"./services/workday-manager": {
|
|
193
|
+
"types": "./dist/services/workday-manager.d.ts",
|
|
194
|
+
"default": "./dist/services/workday-manager.js"
|
|
195
|
+
},
|
|
190
196
|
"./services/workday-start": {
|
|
191
197
|
"types": "./dist/services/workday-start.d.ts",
|
|
192
198
|
"default": "./dist/services/workday-start.js"
|