@treeseed/core 0.4.13 → 0.5.2

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.
Files changed (81) hide show
  1. package/dist/agents/adapters/notification.d.ts +16 -1
  2. package/dist/agents/adapters/notification.js +31 -1
  3. package/dist/agents/adapters/research.d.ts +13 -1
  4. package/dist/agents/adapters/research.js +35 -1
  5. package/dist/agents/contracts/run.d.ts +1 -0
  6. package/dist/agents/kernel/agent-kernel.d.ts +2 -2
  7. package/dist/agents/kernel/agent-kernel.js +10 -3
  8. package/dist/agents/kernel/trigger-resolver.d.ts +1 -0
  9. package/dist/agents/kernel/trigger-resolver.js +5 -1
  10. package/dist/agents/runtime-types.d.ts +1 -0
  11. package/dist/api/app.js +10 -0
  12. package/dist/api/auth/d1-store.js +5 -0
  13. package/dist/api/auth/memory-provider.js +6 -1
  14. package/dist/api/auth/rbac.d.ts +2 -2
  15. package/dist/api/auth/rbac.js +2 -0
  16. package/dist/api/capabilities.d.ts +9 -0
  17. package/dist/api/capabilities.js +33 -0
  18. package/dist/api/operations-routes.d.ts +4 -0
  19. package/dist/api/operations-routes.js +49 -1
  20. package/dist/api/project-routes.d.ts +8 -0
  21. package/dist/api/project-routes.js +586 -0
  22. package/dist/api/types.d.ts +7 -0
  23. package/dist/components/site/NotesList.astro +13 -2
  24. package/dist/components/site/PublishedContentBody.astro +5 -0
  25. package/dist/content.js +77 -9
  26. package/dist/dev.d.ts +2 -2
  27. package/dist/dev.js +0 -15
  28. package/dist/env.yaml +39 -26
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +7 -1
  31. package/dist/launch.d.ts +3 -0
  32. package/dist/launch.js +8 -0
  33. package/dist/layouts/AuthoredEntryLayout.astro +76 -28
  34. package/dist/layouts/ProfileLayout.astro +9 -5
  35. package/dist/middleware.js +11 -0
  36. package/dist/pages/[slug].astro +10 -6
  37. package/dist/pages/agents/[slug].astro +17 -7
  38. package/dist/pages/agents/index.astro +2 -1
  39. package/dist/pages/books/[slug].astro +10 -5
  40. package/dist/pages/books/index.astro +4 -1
  41. package/dist/pages/decisions/[slug].astro +73 -0
  42. package/dist/pages/decisions/index.astro +47 -0
  43. package/dist/pages/docs-runtime/[...slug].astro +102 -0
  44. package/dist/pages/docs-runtime/index.astro +89 -0
  45. package/dist/pages/feed.xml.js +2 -1
  46. package/dist/pages/index.astro +160 -16
  47. package/dist/pages/notes/[slug].astro +10 -5
  48. package/dist/pages/notes/index.astro +6 -3
  49. package/dist/pages/objectives/[slug].astro +27 -9
  50. package/dist/pages/objectives/index.astro +19 -2
  51. package/dist/pages/people/[slug].astro +17 -7
  52. package/dist/pages/people/index.astro +2 -1
  53. package/dist/pages/proposals/[slug].astro +72 -0
  54. package/dist/pages/proposals/index.astro +47 -0
  55. package/dist/pages/questions/[slug].astro +27 -9
  56. package/dist/pages/questions/index.astro +19 -2
  57. package/dist/scripts/dev-platform.js +0 -1
  58. package/dist/scripts/release-verify.js +29 -2
  59. package/dist/scripts/tenant-build.js +4 -1
  60. package/dist/scripts/tenant-check.js +4 -1
  61. package/dist/services/agents.d.ts +1 -12
  62. package/dist/services/agents.js +28 -9
  63. package/dist/services/index.d.ts +0 -2
  64. package/dist/services/index.js +0 -6
  65. package/dist/services/manager.d.ts +4 -4
  66. package/dist/services/manager.js +123 -50
  67. package/dist/services/workday-report.d.ts +3 -3
  68. package/dist/services/workday-start.d.ts +3 -3
  69. package/dist/services/worker-capacity.d.ts +58 -0
  70. package/dist/services/worker-capacity.js +208 -0
  71. package/dist/services/worker.js +70 -13
  72. package/dist/site.js +18 -5
  73. package/dist/tenant/runtime-config.js +8 -1
  74. package/dist/utils/hub-content.js +14 -0
  75. package/dist/utils/published-content.js +13 -0
  76. package/dist/utils/site-config.js +20 -0
  77. package/dist/utils/site-content-runtime.js +185 -0
  78. package/dist/utils/web-cache.js +149 -0
  79. package/package.json +11 -6
  80. package/scripts/verify-driver.mjs +34 -0
  81. package/templates/github/deploy.workflow.yml +11 -1
@@ -3,9 +3,26 @@ import MainLayout from '../../layouts/MainLayout.astro';
3
3
  import SectionIntro from '../../components/site/SectionIntro.astro';
4
4
  import ChronicleList from '../../components/site/ChronicleList.astro';
5
5
  import { getPublishedQuestions, resolveContributorsForEntries } from '../../utils/hub-content';
6
+ import { isPublishedRuntimeContentMode, loadPublishedCollection, resolvePublishedContributor, metadataFromPublishedContent, loadPublishedEntry } from '../../utils/site-content-runtime';
6
7
 
7
- const questions = await getPublishedQuestions();
8
- const contributors = await resolveContributorsForEntries(questions);
8
+ const publishedRuntime = isPublishedRuntimeContentMode();
9
+ const questions = publishedRuntime
10
+ ? (await loadPublishedCollection(Astro.locals, 'questions')).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
11
+ : await getPublishedQuestions();
12
+ const contributors = publishedRuntime
13
+ ? new Map(
14
+ await Promise.all(
15
+ questions.map(async (question) => {
16
+ const detail = await loadPublishedEntry(Astro.locals, 'questions', question.id);
17
+ const contributor = await resolvePublishedContributor(
18
+ Astro.locals,
19
+ metadataFromPublishedContent(detail?.content)?.primaryContributor,
20
+ );
21
+ return [question.id, contributor ?? null] as const;
22
+ }),
23
+ ),
24
+ )
25
+ : await resolveContributorsForEntries(questions);
9
26
  ---
10
27
 
11
28
  <MainLayout title="Questions" description="Open questions guiding the TreeSeed working site." currentPath="/questions/">
@@ -16,7 +16,6 @@ function parseSurface(value) {
16
16
  || value === 'api'
17
17
  || value === 'manager'
18
18
  || value === 'worker'
19
- || value === 'agents'
20
19
  || value === 'services'
21
20
  || value === 'integrated') {
22
21
  return value;
@@ -9,9 +9,9 @@ const forbiddenPatterns = [
9
9
  /['"`](?:\.\.\/|\.\/)[^'"`\n]*src\/[^'"`\n]*\.(?:[cm]?js|ts|tsx|json|astro|css)['"`]/,
10
10
  /['"`][^'"`\n]*\/packages\/[^'"`\n]*\/src\/[^'"`\n]*['"`]/,
11
11
  ];
12
- function run(command, args) {
12
+ function run(command, args, cwd = packageRoot) {
13
13
  const result = spawnSync(command, args, {
14
- cwd: packageRoot,
14
+ cwd,
15
15
  stdio: 'inherit',
16
16
  env: process.env,
17
17
  });
@@ -19,6 +19,32 @@ function run(command, args) {
19
19
  process.exit(result.status ?? 1);
20
20
  }
21
21
  }
22
+ function assertNoLocalDependencyLinks() {
23
+ const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
24
+ for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
25
+ for (const [dependencyName, version] of Object.entries(packageJson[sectionName] ?? {})) {
26
+ if (version.startsWith('workspace:') || version.startsWith('file:')) {
27
+ throw new Error(`package.json ${sectionName}.${dependencyName} must not use local dependency specifiers: ${version}`);
28
+ }
29
+ }
30
+ }
31
+ const lockfile = JSON.parse(readFileSync(resolve(packageRoot, 'package-lock.json'), 'utf8'));
32
+ for (const [entryKey, entryValue] of Object.entries(lockfile.packages ?? {})) {
33
+ if (entryKey.startsWith('../') || entryKey.includes('/../')) {
34
+ throw new Error(`package-lock.json contains forbidden local package entry: ${entryKey}`);
35
+ }
36
+ if (entryValue.link) {
37
+ throw new Error(`package-lock.json contains forbidden linked dependency entry: ${entryKey}`);
38
+ }
39
+ const resolved = entryValue.resolved ?? '';
40
+ if (resolved.startsWith('../')
41
+ || resolved.startsWith('./')
42
+ || resolved.startsWith('file:')
43
+ || resolved.startsWith('workspace:')) {
44
+ throw new Error(`package-lock.json contains forbidden local resolution for ${entryKey}: ${resolved}`);
45
+ }
46
+ }
47
+ }
22
48
  function walkFiles(root) {
23
49
  const files = [];
24
50
  for (const entry of readdirSync(root, { withFileTypes: true })) {
@@ -43,6 +69,7 @@ function scanDirectory(root) {
43
69
  }
44
70
  }
45
71
  }
72
+ assertNoLocalDependencyLinks();
46
73
  run('npm', ['run', 'lint']);
47
74
  scanDirectory(resolve(packageRoot, 'dist'));
48
75
  run('npm', ['run', 'test:unit']);
@@ -1,7 +1,10 @@
1
1
  import { astroBin, createProductionBuildEnv, packageScriptPath, runNodeBinary, runNodeScript } from './package-tools.js';
2
2
  process.env.TREESEED_LOCAL_DEV_MODE = process.env.TREESEED_LOCAL_DEV_MODE ?? 'cloudflare';
3
+ const publishedRuntime = process.env.TREESEED_CONTENT_SERVING_MODE === 'published_runtime';
3
4
  runNodeScript(packageScriptPath('patch-starlight-content-path'), [], { cwd: process.cwd() });
4
- runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
5
+ if (!publishedRuntime) {
6
+ runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
7
+ }
5
8
  runNodeBinary(astroBin, ['build'], {
6
9
  cwd: process.cwd(),
7
10
  env: createProductionBuildEnv({
@@ -1,6 +1,9 @@
1
1
  import { createProductionBuildEnv, packageScriptPath, runNodeScript } from './package-tools.js';
2
+ const publishedRuntime = process.env.TREESEED_CONTENT_SERVING_MODE === 'published_runtime';
2
3
  runNodeScript(packageScriptPath('patch-starlight-content-path'), [], { cwd: process.cwd() });
3
- runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
4
+ if (!publishedRuntime) {
5
+ runNodeScript(packageScriptPath('aggregate-book'), [], { cwd: process.cwd() });
6
+ }
4
7
  runNodeScript(packageScriptPath('tenant-build'), [], {
5
8
  cwd: process.cwd(),
6
9
  env: createProductionBuildEnv(),
@@ -1,22 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  export declare function resolveAgentsServiceConfig(): {
3
3
  serviceName: string;
4
- marketBaseUrl: string;
5
- projectId: string;
6
- runnerToken: string;
7
- runnerId: string;
8
- batchSize: number;
9
4
  pollIntervalMs: number;
10
5
  };
11
6
  export declare function runAgentsCycle(): Promise<{
12
7
  ok: boolean;
13
8
  processed: number;
14
- idle: boolean;
15
- reason: string;
16
- } | {
17
- ok: boolean;
18
- processed: number;
19
- idle?: undefined;
20
- reason?: undefined;
9
+ results: any[];
21
10
  }>;
22
11
  export declare function startAgentsLoop(): Promise<void>;
@@ -1,21 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from "node:url";
3
- import { runRemoteRunnerCycle, startRemoteRunnerLoop, resolveRemoteRunnerConfig } from "./remote-runner.js";
3
+ import { AgentKernel } from "../agents/kernel/agent-kernel.js";
4
+ import { createServiceSdk, resolveServiceRepoRoot } from "./common.js";
5
+ function integerFromEnv(name, fallback) {
6
+ const value = process.env[name];
7
+ if (!value) return fallback;
8
+ const parsed = Number.parseInt(value, 10);
9
+ return Number.isFinite(parsed) ? parsed : fallback;
10
+ }
4
11
  function resolveAgentsServiceConfig() {
5
12
  return {
6
- ...resolveRemoteRunnerConfig(),
7
- serviceName: process.env.TREESEED_AGENTS_SERVICE_NAME?.trim() || "agents"
13
+ serviceName: process.env.TREESEED_AGENTS_SERVICE_NAME?.trim() || "agents",
14
+ pollIntervalMs: integerFromEnv("TREESEED_AGENTS_POLL_INTERVAL_MS", 3e4)
8
15
  };
9
16
  }
10
17
  async function runAgentsCycle() {
11
- return runRemoteRunnerCycle({
12
- config: resolveAgentsServiceConfig()
13
- });
18
+ const sdk = createServiceSdk();
19
+ const kernel = new AgentKernel(sdk, resolveServiceRepoRoot());
20
+ const results = await kernel.runCycle();
21
+ return {
22
+ ok: true,
23
+ processed: results.length,
24
+ results
25
+ };
14
26
  }
15
27
  async function startAgentsLoop() {
16
- return startRemoteRunnerLoop({
17
- config: resolveAgentsServiceConfig()
18
- });
28
+ const config = resolveAgentsServiceConfig();
29
+ for (; ; ) {
30
+ try {
31
+ await runAgentsCycle();
32
+ } catch (error) {
33
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
34
+ `);
35
+ }
36
+ await new Promise((resolve) => setTimeout(resolve, config.pollIntervalMs));
37
+ }
19
38
  }
20
39
  const currentFile = fileURLToPath(import.meta.url);
21
40
  const entryFile = process.argv[1] ?? "";
@@ -1,7 +1,5 @@
1
1
  export { runManagerAction, runManagerCycle, startManagerLoop } from './manager.ts';
2
2
  export { runWorkerCycle, startWorkerLoop } from './worker.ts';
3
- export { runRemoteRunnerCycle, startRemoteRunnerLoop } from './remote-runner.ts';
4
- export { runAgentsCycle, startAgentsLoop } from './agents.ts';
5
3
  export { runWorkdayStart } from './workday-start.ts';
6
4
  export { runWorkdayReport } from './workday-report.ts';
7
5
  export { createWorkerPoolScaler, RailwayWorkerPoolScaler, NoopWorkerPoolScaler } from './worker-pool-scaler.ts';
@@ -1,7 +1,5 @@
1
1
  import { runManagerAction, runManagerCycle, startManagerLoop } from "./manager.js";
2
2
  import { runWorkerCycle, startWorkerLoop } from "./worker.js";
3
- import { runRemoteRunnerCycle, startRemoteRunnerLoop } from "./remote-runner.js";
4
- import { runAgentsCycle, startAgentsLoop } from "./agents.js";
5
3
  import { runWorkdayStart } from "./workday-start.js";
6
4
  import { runWorkdayReport } from "./workday-report.js";
7
5
  import { createWorkerPoolScaler, RailwayWorkerPoolScaler, NoopWorkerPoolScaler } from "./worker-pool-scaler.js";
@@ -9,15 +7,11 @@ export {
9
7
  NoopWorkerPoolScaler,
10
8
  RailwayWorkerPoolScaler,
11
9
  createWorkerPoolScaler,
12
- runAgentsCycle,
13
10
  runManagerAction,
14
11
  runManagerCycle,
15
- runRemoteRunnerCycle,
16
12
  runWorkdayReport,
17
13
  runWorkdayStart,
18
14
  runWorkerCycle,
19
- startAgentsLoop,
20
15
  startManagerLoop,
21
- startRemoteRunnerLoop,
22
16
  startWorkerLoop
23
17
  };
@@ -48,7 +48,7 @@ export declare function runManagerAction(options?: {
48
48
  mode: "reconcile";
49
49
  managerId: string;
50
50
  projectId: string;
51
- environment: "local" | "prod" | "staging";
51
+ environment: "local" | "staging" | "prod";
52
52
  insideWorkWindow: boolean;
53
53
  workPolicy: WorkdayPolicy;
54
54
  workDay: Record<string, unknown>;
@@ -98,7 +98,7 @@ export declare function runManagerAction(options?: {
98
98
  title: string;
99
99
  };
100
100
  projectId: string;
101
- environment: "local" | "prod" | "staging";
101
+ environment: "local" | "staging" | "prod";
102
102
  workDayId: string;
103
103
  state: string;
104
104
  totalTasks: number;
@@ -165,7 +165,7 @@ export declare function runManagerAction(options?: {
165
165
  title: string;
166
166
  };
167
167
  projectId: string;
168
- environment: "local" | "prod" | "staging";
168
+ environment: "local" | "staging" | "prod";
169
169
  workDayId: string;
170
170
  state: string;
171
171
  totalTasks: number;
@@ -226,7 +226,7 @@ export declare function runManagerCycle(options?: {
226
226
  mode: "reconcile";
227
227
  managerId: string;
228
228
  projectId: string;
229
- environment: "local" | "prod" | "staging";
229
+ environment: "local" | "staging" | "prod";
230
230
  insideWorkWindow: boolean;
231
231
  workPolicy: WorkdayPolicy;
232
232
  workDay: Record<string, unknown>;
@@ -3,7 +3,15 @@ import { fileURLToPath } from "node:url";
3
3
  import {
4
4
  createControlPlaneReporter
5
5
  } from "@treeseed/sdk";
6
+ import { loadActiveAgentSpecs } from "../agents/spec-loader.js";
7
+ import { followCursorKey, resolveTriggerDecision } from "../agents/kernel/trigger-resolver.js";
6
8
  import { createQueuePushClient, createServiceSdk, queueEnvelopeForTask, resolveManagerConfig } from "./common.js";
9
+ import {
10
+ applyInteractiveWakeUpOverride,
11
+ applyScaleCooldown,
12
+ collectTaskMetrics,
13
+ computeDesiredWorkerCount
14
+ } from "./worker-capacity.js";
7
15
  import { writeWorkdayContentSnapshot } from "./workday-content.js";
8
16
  import { createWorkerPoolScaler } from "./worker-pool-scaler.js";
9
17
  const DEFAULT_WORK_DAYS = [1, 2, 3, 4, 5];
@@ -167,6 +175,9 @@ function readDate(record, ...keys) {
167
175
  const parsed = new Date(raw);
168
176
  return Number.isFinite(parsed.valueOf()) ? parsed : null;
169
177
  }
178
+ function asRecord(value) {
179
+ return typeof value === "object" && value !== null ? value : {};
180
+ }
170
181
  function parseJsonString(value, fallback = {}) {
171
182
  if (typeof value !== "string" || !value.trim()) {
172
183
  return fallback;
@@ -491,34 +502,6 @@ async function openWorkday(sdk, config, policy, now) {
491
502
  });
492
503
  return created.payload;
493
504
  }
494
- async function collectTaskMetrics(sdk, workDayId) {
495
- const [queuedEnvelope, activeEnvelope] = await Promise.all([
496
- sdk.searchTasks({
497
- workDayId: workDayId ?? void 0,
498
- limit: 500,
499
- state: ["pending", "queued"]
500
- }),
501
- sdk.searchTasks({
502
- workDayId: workDayId ?? void 0,
503
- limit: 500,
504
- state: ["claimed", "running"]
505
- })
506
- ]);
507
- const queuedTasks = asRecords(queuedEnvelope.payload);
508
- const activeTasks = asRecords(activeEnvelope.payload);
509
- const queuedCredits = queuedTasks.reduce((total, task) => {
510
- const payload = parseJson(String(task.payloadJson ?? "{}"), {});
511
- const credits = readNumber(payload, "estimatedCredits") ?? 1;
512
- return total + credits;
513
- }, 0);
514
- return {
515
- queuedTasks,
516
- activeTasks,
517
- queuedCount: queuedTasks.length,
518
- activeLeases: activeTasks.length,
519
- queuedCredits
520
- };
521
- }
522
505
  function remainingCredits(workDay, policy) {
523
506
  if (!workDay) {
524
507
  return policy.dailyTaskCreditBudget;
@@ -531,7 +514,7 @@ function chooseAgentId(agentSpecs) {
531
514
  const preferred = agentSpecs.find((spec) => {
532
515
  const triggers = Array.isArray(spec.triggers) ? spec.triggers : [];
533
516
  return triggers.some((trigger) => {
534
- const type = typeof trigger === "string" ? trigger : readString(trigger, "type");
517
+ const type = typeof trigger === "string" ? trigger : readString(asRecord(trigger), "type");
535
518
  return type === "startup" || type === "schedule";
536
519
  });
537
520
  });
@@ -640,31 +623,107 @@ async function topUpQueuedTasks(sdk, config, policy, workDay, snapshot, now) {
640
623
  remainingCredits: availableCredits
641
624
  };
642
625
  }
643
- function desiredWorkersForSnapshot(policy, metrics) {
644
- const { minWorkers, maxWorkers, targetQueueDepth } = policy.autoscale;
645
- if (metrics.queuedCount <= 0 && metrics.activeLeases <= 0) {
646
- return minWorkers;
626
+ function parseCursorTimestamp(value) {
627
+ if (typeof value !== "string" || !value.trim()) {
628
+ return void 0;
629
+ }
630
+ const timestamp = new Date(value).valueOf();
631
+ return Number.isFinite(timestamp) ? timestamp : void 0;
632
+ }
633
+ function triggerPriority(invocation) {
634
+ switch (invocation.kind) {
635
+ case "message":
636
+ return 90;
637
+ case "follow":
638
+ return 80;
639
+ case "startup":
640
+ return 70;
641
+ case "schedule":
642
+ case "manual":
643
+ default:
644
+ return 60;
647
645
  }
648
- const requiredByQueue = Math.ceil(metrics.queuedCount / Math.max(1, targetQueueDepth));
649
- const minimumActive = metrics.activeLeases > 0 ? 1 : 0;
650
- return Math.max(minWorkers, Math.min(maxWorkers, Math.max(requiredByQueue, minimumActive)));
651
646
  }
652
- function applyScaleCooldown(policy, latestDecision, nextDesired, now) {
653
- if (!latestDecision) {
654
- return nextDesired;
647
+ function triggerTaskIdempotencyKey(workDayId, agent, invocation) {
648
+ if (invocation.kind === "message" && invocation.message?.id) {
649
+ return `${workDayId}:trigger:${agent.slug}:message:${invocation.message.id}`;
655
650
  }
656
- if (nextDesired >= latestDecision.desiredWorkers) {
657
- return nextDesired;
651
+ if (invocation.kind === "follow") {
652
+ return `${workDayId}:trigger:${agent.slug}:follow:${followCursorKey(invocation.followModels)}:${invocation.cursorValue ?? "none"}`;
658
653
  }
659
- const cooldownMs = Math.max(0, policy.autoscale.cooldownSeconds) * 1e3;
660
- if (cooldownMs === 0) {
661
- return nextDesired;
654
+ const triggerKey = readString(
655
+ asRecord(invocation.trigger),
656
+ "name",
657
+ "type"
658
+ ) || invocation.kind;
659
+ return `${workDayId}:trigger:${agent.slug}:${invocation.kind}:${triggerKey}`;
660
+ }
661
+ async function materializeAgentTriggerTasks(sdk, workDay, now) {
662
+ const workDayId = String(workDay.id ?? "");
663
+ if (!workDayId) {
664
+ return [];
665
+ }
666
+ const [{ specs, diagnostics }, existingTasksEnvelope] = await Promise.all([
667
+ loadActiveAgentSpecs(sdk),
668
+ sdk.searchTasks({ workDayId, limit: 1e3 })
669
+ ]);
670
+ const errors = diagnostics.filter((entry) => entry.severity === "error");
671
+ if (errors.length > 0) {
672
+ throw new Error(
673
+ `Agent spec validation failed: ${errors.map((entry) => `${entry.slug}:${entry.field}:${entry.message}`).join(" | ")}`
674
+ );
662
675
  }
663
- const lastChangedAt = new Date(latestDecision.createdAt);
664
- if (!Number.isFinite(lastChangedAt.valueOf())) {
665
- return nextDesired;
676
+ const existingKeys = new Set(
677
+ asRecords(existingTasksEnvelope.payload).map((task) => readString(task, "idempotencyKey", "idempotency_key"))
678
+ );
679
+ const createdTasks = [];
680
+ for (const agent of [...specs].sort((left, right) => left.slug.localeCompare(right.slug))) {
681
+ const scopedSdk = sdk.scopeForAgent(agent);
682
+ const lastRunAt = parseCursorTimestamp((await sdk.getCursor({
683
+ agentSlug: agent.slug,
684
+ cursorKey: "last_run_at"
685
+ })).payload);
686
+ const runsThisCycle = agent.triggerPolicy?.maxRunsPerCycle ?? 1;
687
+ for (let index = 0; index < runsThisCycle; index += 1) {
688
+ const decision = await resolveTriggerDecision({
689
+ agent,
690
+ mode: "auto",
691
+ isRunning: false,
692
+ lastRunAt,
693
+ sdk: scopedSdk
694
+ });
695
+ if (decision.kind !== "ready" || !decision.invocation) {
696
+ break;
697
+ }
698
+ const invocation = decision.invocation;
699
+ const idempotencyKey = triggerTaskIdempotencyKey(workDayId, agent, invocation);
700
+ if (existingKeys.has(idempotencyKey)) {
701
+ continue;
702
+ }
703
+ const created = await sdk.createTask({
704
+ workDayId,
705
+ agentId: agent.slug,
706
+ type: "agent_trigger",
707
+ priority: triggerPriority(invocation),
708
+ idempotencyKey,
709
+ payload: {
710
+ executionKind: "agent_trigger",
711
+ agentSlug: agent.slug,
712
+ invocation,
713
+ createdAt: now.toISOString()
714
+ },
715
+ graphVersion: typeof workDay.graphVersion === "string" ? workDay.graphVersion : null,
716
+ actor: "manager"
717
+ });
718
+ if (!created.payload) {
719
+ continue;
720
+ }
721
+ await maybeEnqueueTask(sdk, created.payload);
722
+ createdTasks.push(created.payload);
723
+ existingKeys.add(idempotencyKey);
724
+ }
666
725
  }
667
- return now.valueOf() - lastChangedAt.valueOf() < cooldownMs ? latestDecision.desiredWorkers : nextDesired;
726
+ return createdTasks;
668
727
  }
669
728
  async function registerHeartbeat(reporter, config, policy, desiredWorkers, metrics) {
670
729
  await reporter.registerAgentPoolHeartbeat({
@@ -878,10 +937,24 @@ async function reconcileManager(options) {
878
937
  if (activeWorkDay && insideWorkWindow && seedResult.remainingCredits > 0) {
879
938
  seedResult = await topUpQueuedTasks(sdk, config, policy, activeWorkDay, currentSnapshot, now);
880
939
  }
940
+ if (activeWorkDay && insideWorkWindow) {
941
+ const triggerTasks = await materializeAgentTriggerTasks(sdk, activeWorkDay, now);
942
+ if (triggerTasks.length > 0) {
943
+ seedResult = {
944
+ ...seedResult,
945
+ createdTasks: [...seedResult.createdTasks, ...triggerTasks]
946
+ };
947
+ }
948
+ }
881
949
  const metrics = await collectTaskMetrics(sdk, activeWorkDay ? String(activeWorkDay.id ?? "") : null);
882
- const rawDesiredWorkers = activeWorkDay ? desiredWorkersForSnapshot(policy, metrics) : 0;
950
+ const rawDesiredWorkers = activeWorkDay ? computeDesiredWorkerCount(policy.autoscale, metrics) : 0;
883
951
  const latestScaleDecision = await sdk.getLatestScaleDecision(config.projectId, config.environment, config.poolName);
884
- const desiredWorkers = applyScaleCooldown(policy, latestScaleDecision.payload, rawDesiredWorkers, now);
952
+ const desiredWorkers = applyInteractiveWakeUpOverride({
953
+ priorityClass: "background",
954
+ queuedCount: metrics.queuedCount,
955
+ currentWorkers: Number(latestScaleDecision.payload?.desiredWorkers ?? 0),
956
+ desiredWorkers: applyScaleCooldown(policy.autoscale, latestScaleDecision.payload, rawDesiredWorkers, now)
957
+ });
885
958
  const scaleDecision = {
886
959
  projectId: config.projectId,
887
960
  environment: config.environment,
@@ -4,7 +4,7 @@ export declare function runWorkdayReport(): Promise<{
4
4
  mode: "reconcile";
5
5
  managerId: string;
6
6
  projectId: string;
7
- environment: "local" | "prod" | "staging";
7
+ environment: "local" | "staging" | "prod";
8
8
  insideWorkWindow: boolean;
9
9
  workPolicy: import("@treeseed/sdk").WorkdayPolicy;
10
10
  workDay: Record<string, unknown>;
@@ -56,7 +56,7 @@ export declare function runWorkdayReport(): Promise<{
56
56
  title: string;
57
57
  };
58
58
  projectId: string;
59
- environment: "local" | "prod" | "staging";
59
+ environment: "local" | "staging" | "prod";
60
60
  workDayId: string;
61
61
  state: string;
62
62
  totalTasks: number;
@@ -123,7 +123,7 @@ export declare function runWorkdayReport(): Promise<{
123
123
  title: string;
124
124
  };
125
125
  projectId: string;
126
- environment: "local" | "prod" | "staging";
126
+ environment: "local" | "staging" | "prod";
127
127
  workDayId: string;
128
128
  state: string;
129
129
  totalTasks: number;
@@ -4,7 +4,7 @@ export declare function runWorkdayStart(): Promise<{
4
4
  mode: "reconcile";
5
5
  managerId: string;
6
6
  projectId: string;
7
- environment: "local" | "prod" | "staging";
7
+ environment: "local" | "staging" | "prod";
8
8
  insideWorkWindow: boolean;
9
9
  workPolicy: import("@treeseed/sdk").WorkdayPolicy;
10
10
  workDay: Record<string, unknown>;
@@ -56,7 +56,7 @@ export declare function runWorkdayStart(): Promise<{
56
56
  title: string;
57
57
  };
58
58
  projectId: string;
59
- environment: "local" | "prod" | "staging";
59
+ environment: "local" | "staging" | "prod";
60
60
  workDayId: string;
61
61
  state: string;
62
62
  totalTasks: number;
@@ -123,7 +123,7 @@ export declare function runWorkdayStart(): Promise<{
123
123
  title: string;
124
124
  };
125
125
  projectId: string;
126
- environment: "local" | "prod" | "staging";
126
+ environment: "local" | "staging" | "prod";
127
127
  workDayId: string;
128
128
  state: string;
129
129
  totalTasks: number;
@@ -0,0 +1,58 @@
1
+ import type { AgentPoolAutoscalePolicy, AgentSdk, ProjectEnvironmentName, ScaleDecision, WorkerPoolScaleResult, WorkerPoolScaler } from '@treeseed/sdk';
2
+ export interface TaskMetricsSnapshot {
3
+ queuedTasks: Array<Record<string, unknown>>;
4
+ activeTasks: Array<Record<string, unknown>>;
5
+ queuedCount: number;
6
+ activeLeases: number;
7
+ queuedCredits: number;
8
+ }
9
+ export interface WorkerPoolIdentity {
10
+ projectId: string;
11
+ environment: ProjectEnvironmentName | 'local';
12
+ poolName: string;
13
+ }
14
+ export interface CapacityAssuranceResult {
15
+ ok: true;
16
+ taskId: string;
17
+ queued: true;
18
+ workerState: 'warm' | 'cold_starting';
19
+ desiredWorkers: number;
20
+ scaleApplied: boolean;
21
+ scaleReason: string;
22
+ scaleDecision: ScaleDecision;
23
+ scaleResult: WorkerPoolScaleResult;
24
+ metrics: TaskMetricsSnapshot;
25
+ }
26
+ export declare function resolveAutoscalePolicyFromEnv(): AgentPoolAutoscalePolicy;
27
+ export declare function resolveWorkerPoolIdentityFromEnv(projectId?: string): WorkerPoolIdentity;
28
+ export declare function computeDesiredWorkerCount(autoscale: AgentPoolAutoscalePolicy, metrics: Pick<TaskMetricsSnapshot, 'queuedCount' | 'activeLeases'>): number;
29
+ export declare function applyScaleCooldown(autoscale: AgentPoolAutoscalePolicy, latestDecision: ScaleDecision | null, nextDesired: number, now: Date): number;
30
+ export declare function applyInteractiveWakeUpOverride(options: {
31
+ priorityClass?: 'interactive' | 'background';
32
+ queuedCount: number;
33
+ currentWorkers: number;
34
+ desiredWorkers: number;
35
+ }): number;
36
+ export declare function collectTaskMetrics(sdk: AgentSdk, workDayId?: string | null): Promise<TaskMetricsSnapshot>;
37
+ export declare function enqueueTaskAndEnsureCapacity(sdk: AgentSdk, request: {
38
+ taskId: string;
39
+ actor?: string;
40
+ queueName?: string;
41
+ deliveryDelaySeconds?: number;
42
+ priorityClass?: 'interactive' | 'background';
43
+ projectId?: string;
44
+ identity?: WorkerPoolIdentity;
45
+ autoscale?: AgentPoolAutoscalePolicy;
46
+ scaler?: WorkerPoolScaler;
47
+ now?: Date;
48
+ enqueueTask: (sdk: AgentSdk, request: {
49
+ taskId: string;
50
+ queueName?: string;
51
+ deliveryDelaySeconds?: number;
52
+ actor?: string;
53
+ }) => Promise<{
54
+ ok: boolean;
55
+ taskId: string;
56
+ queued: boolean;
57
+ }>;
58
+ }): Promise<CapacityAssuranceResult>;