bazis 0.98.33 → 0.99.0

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 (43) hide show
  1. package/cli/templates/module.ts +4 -64
  2. package/cli/templates/passport.ts +14 -18
  3. package/core/agent/AgentContextBuilder.ts +4 -98
  4. package/core/agent/AgentRegistry.ts +79 -473
  5. package/core/agent/AgentRuntime.ts +47 -19
  6. package/core/agent/AgentToolExecutor.ts +3 -3
  7. package/core/agent/README.md +89 -181
  8. package/core/agent/index.ts +3 -31
  9. package/core/agent/internal/AgentExecutionDriver.ts +37 -392
  10. package/core/agent/internal/AgentExecutionState.ts +1 -9
  11. package/core/agent/metadata.ts +8 -303
  12. package/core/agent/semantic.ts +0 -4
  13. package/core/agent/session/contracts.ts +5 -7
  14. package/core/agent/session/entities.ts +0 -1
  15. package/core/cache/COMPATIBILITY.md +4 -4
  16. package/core/cache/README.md +2 -2
  17. package/core/cache/SPEC.md +30 -48
  18. package/core/cache/decorators/Cacheable.ts +4 -3
  19. package/core/cache/decorators/CacheableRedis.ts +3 -4
  20. package/core/cache/di/classProviderHook.ts +31 -0
  21. package/core/cache/distributed/DistributedCache.ts +96 -25
  22. package/core/cache/distributed/IDistributedCache.ts +9 -0
  23. package/core/cache/distributed/codecs.ts +49 -2
  24. package/core/cache/distributed/index.ts +1 -1
  25. package/core/cache/errors/CacheError.ts +17 -2
  26. package/core/cache/http/outputRedisCacheMiddleware.ts +11 -2
  27. package/core/cache/index.ts +2 -6
  28. package/core/cache/services/cacheProxy.ts +6 -2
  29. package/core/generatedRuntime.ts +0 -31
  30. package/core/infra/cache/RedisDistributedCacheBackend.ts +3 -0
  31. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  32. package/core/infra/connectors/llm.ts +15 -1
  33. package/core/infra/connectors/llmRouter.ts +4 -0
  34. package/core/infra/connectors/redis.ts +9 -1
  35. package/core/scripts/di-generate.ts +28 -42
  36. package/core/scripts/tool-codegen.ts +122 -0
  37. package/index.ts +3 -24
  38. package/package.json +1 -1
  39. package/core/agent/decorators/Agent.ts +0 -11
  40. package/core/agent/decorators/Prompt.ts +0 -11
  41. package/core/agent/decorators/Task.ts +0 -29
  42. package/core/agent/generated/catalog.ts +0 -26
  43. package/core/scripts/agent-codegen.ts +0 -1166
@@ -1,7 +1,8 @@
1
+ import { DiError } from "../../di";
1
2
  /** Base error of the cache module. */
2
3
  export class CacheError extends Error {
3
- public constructor(message: string) {
4
- super(message);
4
+ public constructor(message: string, options?: ErrorOptions) {
5
+ super(message, options);
5
6
  this.name = new.target.name;
6
7
  }
7
8
  }
@@ -12,5 +13,19 @@ export class CacheKeyError extends CacheError {}
12
13
  /** Invalid value (size limit exceeded and the like). */
13
14
  export class CacheValueError extends CacheError {}
14
15
 
16
+ /**
17
+ * The distributed store did not answer in time or failed. On the cache-aside
18
+ * path (`getOrCreateAsync`, `@CacheableRedis`) the value is computed without
19
+ * the cache; explicit operations (`get`, `remove`, `evictByTag`) throw it.
20
+ */
21
+ export class CacheUnavailableError extends CacheError {}
22
+
15
23
  /** The limit of unfinished factories is exhausted; the new factory was not started. */
16
24
  export class CacheCapacityError extends CacheError {}
25
+
26
+ /**
27
+ * A cache declaration that can never work, found while the container is built
28
+ * (a cached scoped method without a key, a cached transient service). A DI
29
+ * error, so the start prints it as a configuration error.
30
+ */
31
+ export class CacheDeclarationError extends DiError {}
@@ -1,5 +1,5 @@
1
1
  import type { HttpMiddleware } from "../../http";
2
- import { CacheError } from "../errors/CacheError";
2
+ import { CacheError, CacheUnavailableError } from "../errors/CacheError";
3
3
  import { DISTRIBUTED_OUTPUT_CACHE } from "../tokens/DISTRIBUTED_CACHE";
4
4
  import {
5
5
  DEFAULT_OUTPUT_CACHE_METHODS,
@@ -14,6 +14,7 @@ import {
14
14
  resolveCachedHttpPayloadReadOptions,
15
15
  responseAllowsServerCache,
16
16
  responseToCachedPayload,
17
+ type CachedHttpPayload,
17
18
  } from "./CachedHttpPayload";
18
19
 
19
20
  export interface OutputRedisCacheMiddlewareOptions {
@@ -81,7 +82,15 @@ export function outputRedisCacheMiddleware(options: OutputRedisCacheMiddlewareOp
81
82
  }
82
83
 
83
84
  const key = buildRedisOutputCacheKey(ctx, routeName, config);
84
- const cached = await cache.get(key);
85
+ let cached: CachedHttpPayload | undefined;
86
+ try {
87
+ cached = await cache.get(key);
88
+ } catch (error) {
89
+ // The cache is optional: an unavailable store serves the route uncached.
90
+ if (!(error instanceof CacheUnavailableError)) throw error;
91
+ await next();
92
+ return;
93
+ }
85
94
  if (cached !== undefined) {
86
95
  ctx.response = applyClientCacheHeaders(cachedPayloadToResponse(cached, { hit: true }), clientCache);
87
96
  return;
@@ -51,7 +51,7 @@ export {
51
51
  type DistributedCacheVersionCheck,
52
52
  type DistributedCachePing,
53
53
  type DistributedCacheSetOptions,
54
- type IDistributedCache,
54
+ IDistributedCache,
55
55
  } from "./distributed";
56
56
  export {
57
57
  DISTRIBUTED_CACHE_BACKEND,
@@ -66,7 +66,7 @@ export {
66
66
  type CacheModuleConfig,
67
67
  type CacheOutputCacheOptions,
68
68
  } from "./internal/buildCacheProviders";
69
- export { CacheError, CacheKeyError, CacheValueError, CacheCapacityError } from "./errors/CacheError";
69
+ export { CacheError, CacheKeyError, CacheValueError, CacheCapacityError, CacheDeclarationError, CacheUnavailableError } from "./errors/CacheError";
70
70
 
71
71
  export { OutputCache, type OutputCacheOptions as OutputCacheDecoratorOptions } from "./decorators/OutputCache";
72
72
  export {
@@ -91,7 +91,3 @@ export {
91
91
  } from "./internal/resolveCachePolicy";
92
92
  export { buildOutputCacheKey, OUTPUT_CACHE_PRINCIPAL_STATE_KEY } from "./http/buildOutputCacheKey";
93
93
  export { wrapCachedService, type CacheProxyOptions } from "./services/cacheProxy";
94
- export {
95
- cachedSingleton,
96
- cachedScoped,
97
- } from "./providers/cachedProviders";
@@ -11,7 +11,7 @@ import {
11
11
  } from "../decorators/cacheableRedisMetadata";
12
12
  import type { IDistributedCache } from "../distributed/IDistributedCache";
13
13
  import type { NamedCacheRegistry } from "../distributed/NamedCacheRegistry";
14
- import { CacheError } from "../errors/CacheError";
14
+ import { CacheError, CacheValueError } from "../errors/CacheError";
15
15
  import type { ICache } from "../ICache";
16
16
  import { normalizeCacheKey } from "../internal/normalizeCacheKey";
17
17
  import { AutomaticCacheKey } from "../internal/AutomaticCacheKey";
@@ -141,7 +141,11 @@ function wrapDistributedMethod(
141
141
  return result instanceof Promise ? await result : result;
142
142
  },
143
143
  { ttlSeconds: options.seconds, tags: options.tags, lockSeconds: options.lockSeconds },
144
- );
144
+ ).catch((error: unknown) => {
145
+ // Name the method: "value.at is a Date" alone does not say where.
146
+ if (error instanceof CacheValueError) throw new CacheValueError(`${className}.${String(methodName)}: ${error.message}`, { cause: error });
147
+ throw error;
148
+ });
145
149
  };
146
150
  }
147
151
 
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import type { AgentMetadataIndex } from "./agent/AgentRegistry";
3
2
  import { registerGeneratedClassDeps, restoreGeneratedClassDeps, snapshotGeneratedClassDeps, type GeneratedClassDependency } from "./di/module/autoDeps";
4
3
  import { registerGeneratedBindings, restoreGeneratedBindings, snapshotGeneratedBindings } from "./http/Binding/autoBindings";
5
4
  import { registerRequestModelShape, restoreRequestModelRegistry, snapshotRequestModelRegistry, type RequestModelClass, type RequestModelShape } from "./http/Binding/requestModelRegistry";
@@ -27,10 +26,6 @@ type GeneratedRuntimeModule = {
27
26
  readonly registerBazisGeneratedRuntime?: () => void | Promise<void>;
28
27
  };
29
28
 
30
- type GeneratedAgentCatalogModule = {
31
- readonly GENERATED_AGENT_METADATA?: AgentMetadataIndex;
32
- };
33
-
34
29
  let runtimeLoad: Promise<void> | undefined;
35
30
 
36
31
  export interface BazisGeneratedTargetDescriptor {
@@ -42,15 +37,12 @@ export interface BazisGeneratedTargetDescriptor {
42
37
  readonly listModels: readonly ListModelClass[];
43
38
  readonly openApi: GeneratedOpenApiMetadata;
44
39
  readonly openApiSchemaModels: readonly (readonly [GeneratedOpenApiSchemaModel, string])[];
45
- /** Target-local generated Agent/Tool/Prompt metadata. */
46
- readonly agentMetadata?: AgentMetadataIndex;
47
40
  /** Package-internal exact-class metadata attached atomically with this target. */
48
41
  readonly providerAttachments?: readonly GeneratedProviderAttachmentV1[];
49
42
  }
50
43
 
51
44
  const committedTargetDescriptors = new Set<string>();
52
45
  const targetDescriptorLoads = new Map<string, Promise<void>>();
53
- const committedTargetDescriptorsById = new Map<string, BazisGeneratedTargetDescriptor>();
54
46
  const explicitlyActivatedTargetIds = new Set<string>();
55
47
 
56
48
  /**
@@ -87,7 +79,6 @@ export function registerBazisGeneratedTargetDescriptor(descriptor: BazisGenerate
87
79
  for (const [model, name] of descriptor.openApiSchemaModels) registerGeneratedOpenApiSchemaModel(model, name);
88
80
  commitGeneratedProviderAttachments(preparedAttachments);
89
81
  committedTargetDescriptors.add(descriptor.id);
90
- committedTargetDescriptorsById.set(descriptor.id, descriptor);
91
82
  explicitlyActivatedTargetIds.add(descriptor.id);
92
83
  } catch (error) {
93
84
  restoreGeneratedClassDeps(snapshots.classDeps);
@@ -136,28 +127,6 @@ export function loadBazisGeneratedRuntime(): Promise<void> {
136
127
  return runtimeLoad;
137
128
  }
138
129
 
139
- export async function loadBazisGeneratedAgentMetadata(
140
- targets: readonly Class<object>[] = [],
141
- ): Promise<AgentMetadataIndex | undefined> {
142
- const registered = selectRegisteredAgentMetadata(targets);
143
- if (registered !== undefined || explicitlyActivatedTargetIds.size > 0) return registered;
144
- const module = await importGeneratedModule<GeneratedAgentCatalogModule>("agentCatalog");
145
- return module?.GENERATED_AGENT_METADATA;
146
- }
147
-
148
- function selectRegisteredAgentMetadata(targets: readonly Class<object>[]): AgentMetadataIndex | undefined {
149
- const candidates = [...committedTargetDescriptorsById.values()]
150
- .map((descriptor) => descriptor.agentMetadata)
151
- .filter((metadata): metadata is AgentMetadataIndex => metadata !== undefined);
152
- if (candidates.length === 0) return undefined;
153
- for (const metadata of candidates) {
154
- for (const target of targets) {
155
- if (metadata.agents.has(target) || metadata.tools.has(target) || metadata.prompts.has(target)) return metadata;
156
- }
157
- }
158
- return candidates.length === 1 ? candidates[0] : undefined;
159
- }
160
-
161
130
  async function loadRuntimeOnce(): Promise<void> {
162
131
  const module = await importGeneratedModule<GeneratedRuntimeModule>("runtime");
163
132
  await module?.registerBazisGeneratedRuntime?.();
@@ -25,6 +25,8 @@ export interface RedisDistributedCacheTuning {
25
25
  readonly maxValueBytes?: number;
26
26
  /** Polling interval (ms) while waiting for another worker's result. */
27
27
  readonly pollIntervalMs?: number;
28
+ /** Bound of one Redis operation (ms); on expiry the method runs without the cache. Default 1000. */
29
+ readonly operationTimeoutMs?: number;
28
30
  }
29
31
 
30
32
  const DEFAULT_KEY_PREFIX = "bazis:cache:";
@@ -59,6 +61,7 @@ export class RedisDistributedCacheBackend implements DistributedCacheStores {
59
61
  maxKeyLength: tuning.maxKeyLength,
60
62
  maxValueBytes: tuning.maxValueBytes,
61
63
  pollIntervalMs: tuning.pollIntervalMs,
64
+ operationTimeoutMs: tuning.operationTimeoutMs,
62
65
  };
63
66
 
64
67
  const output = new DistributedCache<CachedHttpPayload>(driver, httpPayloadCacheCodec, {
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.98.33" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.99.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -1,5 +1,6 @@
1
1
  import type { AppConfig, ConfigRegistry, Secret } from "../../kernel";
2
- import { AGENT_MODEL_PROVIDER, type AgentModelProvider } from "../../agent";
2
+ import { AGENT_MODEL_PROVIDER, AGENT_RUNTIME_OPTIONS, AgentRuntime, type AgentModelProvider } from "../../agent";
3
+ import { DI, SERVICE_PROVIDER, type ProviderDefinition } from "../../di";
3
4
  import { reader, requireValue } from "../connectorConfig";
4
5
  import { InfraError, type InfraConnector } from "../InfraConnector";
5
6
  import { identifyConnector } from "../connectorIdentity";
@@ -54,6 +55,17 @@ export function resolveLlmConnectionOptions<T extends LlmConfigShape>(
54
55
  };
55
56
  }
56
57
 
58
+ /**
59
+ * `AgentRuntime` on top of the connector's model provider, injectable by type.
60
+ * Options come from `AGENT_RUNTIME_OPTIONS` when the application registers it.
61
+ */
62
+ export const agentRuntimeProviders: readonly ProviderDefinition[] = Object.freeze([
63
+ DI.singleton(DI.factoryProviderWithResolver(AgentRuntime, [AGENT_MODEL_PROVIDER], (resolver, model: AgentModelProvider) => {
64
+ const services = resolver.resolve(SERVICE_PROVIDER);
65
+ return new AgentRuntime(services, model, services.has(AGENT_RUNTIME_OPTIONS) ? services.resolve(AGENT_RUNTIME_OPTIONS) : {});
66
+ })),
67
+ ]);
68
+
57
69
  export function llmConnect<T extends LlmConfigShape>(
58
70
  config: AppConfig<T>,
59
71
  adapter: LlmProviderAdapter,
@@ -62,6 +74,8 @@ export function llmConnect<T extends LlmConfigShape>(
62
74
  return identifyConnector({
63
75
  token: AGENT_MODEL_PROVIDER,
64
76
  config,
77
+ providers: agentRuntimeProviders,
78
+ exports: [AgentRuntime],
65
79
  create(configs) {
66
80
  return adapter.create(resolveLlmConnectionOptions(config, options, configs));
67
81
  },
@@ -1,6 +1,7 @@
1
1
  import type { AppConfig } from "../../kernel";
2
2
  import {
3
3
  AGENT_MODEL_PROVIDER,
4
+ AgentRuntime,
4
5
  agentModelRequest,
5
6
  type AgentModelProvider,
6
7
  type AgentModelProviderContext,
@@ -11,6 +12,7 @@ import { InfraError, type InfraConnector } from "../InfraConnector";
11
12
  import { redactSensitiveText } from "../../../library/redaction";
12
13
  import { awaitAbortable } from "../../kernel/internal/awaitAbortable";
13
14
  import {
15
+ agentRuntimeProviders,
14
16
  resolveLlmConnectionOptions,
15
17
  type LlmConfigShape,
16
18
  type LlmConnectionOptions,
@@ -408,6 +410,8 @@ export function llmRouter(
408
410
  return {
409
411
  token: AGENT_MODEL_PROVIDER,
410
412
  config: configs,
413
+ providers: agentRuntimeProviders,
414
+ exports: [AgentRuntime],
411
415
  create(configs) {
412
416
  // The router must have an owner before adapters allocate resources.
413
417
  // Initialization happens inside an awaited operation, so rollback can
@@ -3,6 +3,7 @@ import {
3
3
  DISTRIBUTED_CACHE_BACKEND,
4
4
  DISTRIBUTED_OUTPUT_CACHE,
5
5
  DISTRIBUTED_SERVICE_CACHE,
6
+ IDistributedCache,
6
7
  type DistributedCacheStores,
7
8
  } from "../../cache";
8
9
  import { createToken, DI, type InjectionToken, type ProviderDefinition } from "../../di";
@@ -60,6 +61,13 @@ function distributedCacheProviders(tuning: RedisDistributedCacheTuning, token: I
60
61
  backend.serviceCache,
61
62
  ),
62
63
  ),
64
+ // This connector's one connection by type, for services that evict tags or
65
+ // read values; its name may be "default" or the configured `connection`.
66
+ DI.singleton(
67
+ DI.factoryProvider(IDistributedCache, [DISTRIBUTED_CACHE_BACKEND], (backend: DistributedCacheStores) =>
68
+ backend.serviceCache.resolve(backend.serviceCache.connectionNames[0]),
69
+ ),
70
+ ),
63
71
  ];
64
72
  }
65
73
 
@@ -91,7 +99,7 @@ export function redisConnect<T extends RedisConfigShape>(
91
99
  token,
92
100
  config,
93
101
  providers: cacheProviders,
94
- exports: cacheProviders ? [DISTRIBUTED_CACHE_BACKEND, DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE] : undefined,
102
+ exports: cacheProviders ? [DISTRIBUTED_CACHE_BACKEND, DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE, IDistributedCache] : undefined,
95
103
  create(configs) {
96
104
  return new RedisClient(redisUrl(reader(config, configs).get("url")));
97
105
  },
@@ -7,7 +7,7 @@ import {
7
7
  type OpenApiCodegenOperationSpec,
8
8
  type OpenApiCodegenSchema,
9
9
  } from "../../library/openapi/codegen";
10
- import { generateAgentMetadataCatalog, type AgentCodegenSource } from "./agent-codegen";
10
+ import { collectToolSchemaModels, type ToolCodegenSource } from "./tool-codegen";
11
11
  import {
12
12
  analyzeRequestModelHydration,
13
13
  type RequestModelHydration,
@@ -30,8 +30,6 @@ const FRAMEWORK_SOURCE = ((): string | null => {
30
30
  try { return realpathSync("src/bazis") === FRAMEWORK_DIR ? "src/bazis" : null; } catch { return null; }
31
31
  })();
32
32
  const FRAMEWORK_ROOT = FRAMEWORK_SOURCE ?? "src/bazis";
33
- const CORE_AGENT_CATALOG_FILE = `${FRAMEWORK_ROOT}/core/agent/generated/catalog.ts`;
34
- const CORE_AGENT_GENERATED_DIR = `${FRAMEWORK_ROOT}/core/agent/generated`;
35
33
  const isFrameworkSourcePath = (file: string): boolean => FRAMEWORK_SOURCE !== null && file.startsWith(`${FRAMEWORK_SOURCE}/`);
36
34
  const APP_GENERATED_DIR = "src/generated/bazis";
37
35
  let activeGeneratedDir = APP_GENERATED_DIR;
@@ -209,10 +207,10 @@ let ambiguousClassNames = new Set<string>();
209
207
  // Exact roots that do not need convention name resolution: @RequestModel()
210
208
  // classes used by generated transport schemas.
211
209
  let explicitRequestModelRoots = new Set<ts.ClassDeclaration>();
212
- let agentCodegenSources: AgentCodegenSource[] = [];
210
+ let toolCodegenSources: ToolCodegenSource[] = [];
213
211
 
214
212
  const ROUTE_DECORATORS = new Set(["Get", "Post", "Put", "Patch", "Delete", "Options", "Head", "All"]);
215
- const CODEGEN_CHANNEL_DECORATORS = new Set(["Controller", "GrpcController", "WebSocketGateway", "Agent", "Tool", "Prompt", "RequestModel", "UiProfile"]);
213
+ const CODEGEN_CHANNEL_DECORATORS = new Set(["Controller", "GrpcController", "WebSocketGateway", "Tool", "RequestModel", "UiProfile"]);
216
214
  const PRIMITIVE_QUERY_TYPES: Record<string, HttpBindingSpec["type"]> = {
217
215
  string: "string",
218
216
  number: "number",
@@ -251,12 +249,7 @@ const BUILTIN_DEPENDENCY_NAMES = new Set([
251
249
  function isScannableFile(filePath: string): boolean {
252
250
  if (filePath.startsWith("../") || path.isAbsolute(filePath)) return false;
253
251
  if (!filePath.endsWith(".ts") || filePath.endsWith(".d.ts") || /\.(test|spec)\.ts$/.test(filePath)) return false;
254
- if (
255
- filePath === CORE_AGENT_CATALOG_FILE ||
256
- filePath.startsWith(`${APP_GENERATED_DIR}/`)
257
- ) {
258
- return false;
259
- }
252
+ if (filePath.startsWith(`${APP_GENERATED_DIR}/`)) return false;
260
253
  return !/(^|\/)(test|tests|__tests__|fixture|fixtures|__fixtures__|generated|node_modules|vendor|dist|build|bin|coverage|examples)(\/|$)/.test(filePath) && !filePath.startsWith("admin-ui/");
261
254
  }
262
255
 
@@ -414,7 +407,7 @@ function sourceHasCodegenCandidate(source: ts.SourceFile): boolean {
414
407
  const decorators = ts.getDecorators(node) ?? [];
415
408
  const constructorDeps = readConstructorTypeNames(node);
416
409
  // Shared all-channel boundary index: any class that feeds DI/HTTP,
417
- // request/list, OpenAPI/UI, Agent/Tool/Prompt analysis must
410
+ // request/list, OpenAPI/UI, Tool analysis must
418
411
  // be owned by exactly one configured target before rendering begins.
419
412
  if (
420
413
  decorators.some((decorator) => {
@@ -456,7 +449,7 @@ function resetTargetAnalysis(files: Set<string>, generatedDir: string): void {
456
449
  httpBindings = {}; httpControllerFiles = {}; openApiSchemas = {}; openApiOperations = {};
457
450
  grpcBindings = new Map();
458
451
  uiProfileResponseModels = new Map(); openApiSchemaModels = new Map(); requestModelClassNames = new Set(); listRequestClassNames = new Set();
459
- classFilesByName = new Map(); classDeclarationsByName = new Map(); ambiguousClassNames = new Set(); explicitRequestModelRoots = new Set(); agentCodegenSources = [];
452
+ classFilesByName = new Map(); classDeclarationsByName = new Map(); ambiguousClassNames = new Set(); explicitRequestModelRoots = new Set(); toolCodegenSources = [];
460
453
  sourceEntries = [...files].sort((a, b) => a.localeCompare(b)).map((filePath) => ({ filePath, source: programFiles.get(filePath) as ts.SourceFile }));
461
454
  sourceFilePathSet = new Set(sourceEntries.map((entry) => entry.filePath));
462
455
  openApiCodegen = createOpenApiCodegenAnalyzer({ checker, sourceFiles: sourceEntries.map((entry) => entry.source), schemaNameForDuplicate: duplicateOpenApiSchemaName });
@@ -478,7 +471,7 @@ async function generateTarget(name: string, reachable: Set<string>, production:
478
471
  if (!FRAMEWORK_INTERNAL_PREFIXES.some((prefix) => filePath.startsWith(prefix))) {
479
472
  collectClassDeps(source);
480
473
  collectGrpcBindings(source);
481
- agentCodegenSources.push({ filePath, sourceFile: source });
474
+ toolCodegenSources.push({ filePath, sourceFile: source });
482
475
  }
483
476
  collectHttpBindings(source); collectOpenApiSchemas(source); collectUiProfileResponseModels(source);
484
477
  }
@@ -487,46 +480,39 @@ async function generateTarget(name: string, reachable: Set<string>, production:
487
480
  const requestModelImports = resolveRequestModelImports();
488
481
  const requestModelHydration = resolveRequestModelHydration(requestModelImports);
489
482
  const listModelImports = resolveListModelImports();
490
- const agent = generateAgentMetadataCatalog(agentCodegenSources, {
491
- generatedDir, frameworkImports: "public", schemaNameForDeclaration: openApiCodegen.schemaNameForDeclaration,
492
- });
493
- const schemaModels = resolveOpenApiSchemaModelImports(agent.schemaModels, agent.schemaNames);
494
- classifyAgentWarnings(agent.warnings); failOnFatalErrors();
483
+ const tools = collectToolSchemaModels(toolCodegenSources, { schemaNameForDeclaration: openApiCodegen.schemaNameForDeclaration });
484
+ const schemaModels = resolveOpenApiSchemaModelImports(tools.schemaModels, tools.schemaNames);
485
+ classifyToolWarnings(tools.warnings); failOnFatalErrors();
495
486
  const analysisMs = performance.now() - startedAt;
496
487
  const renderStartedAt = performance.now();
497
488
  const requestModels = renderRequestModels(requestModelImports, requestModelHydration);
498
489
  const listModels = renderListModels(listModelImports);
499
- const openApi = renderOpenApiMetadata(openApiSchemas, openApiOperations, "app", agent.schemaNames, schemaModels);
490
+ const openApi = renderOpenApiMetadata(openApiSchemas, openApiOperations, "app", tools.schemaNames, schemaModels);
500
491
  stageWrite(`${generatedDir}/deps.ts`, renderTargetDeps());
501
492
  stageWrite(`${generatedDir}/bindings.ts`, renderTargetBindings());
502
493
  stageWrite(`${generatedDir}/httpRequestModels.ts`, requestModels);
503
494
  stageWrite(`${generatedDir}/httpListModels.ts`, listModels);
504
495
  stageWrite(`${generatedDir}/openapi.ts`, openApi);
505
- stageWrite(`${generatedDir}/agentCatalog.ts`, agent.output);
496
+ // Generated before 0.99.0, when agents were classes; nothing imports it now.
497
+ stageRemove(`${generatedDir}/agentCatalog.ts`);
506
498
  stageWrite(`${generatedDir}/runtime.ts`, renderGeneratedRuntime(name));
507
499
  if (production) stageWrite(`${generatedDir}/fingerprint.ts`, renderSourceFingerprint());
508
- // The framework's own empty agent catalog (a fixture of agent.metadata.test)
509
- // is refreshed only when the framework source is part of this project.
510
- if (production) {
511
- if (FRAMEWORK_SOURCE !== null) {
512
- stageWrite(CORE_AGENT_CATALOG_FILE, generateAgentMetadataCatalog([], CORE_AGENT_GENERATED_DIR).output);
513
- }
514
- } else {
500
+ if (!production) {
515
501
  stageWrite(`${generatedDir}/bootstrap.ts`, 'import { registerBazisGeneratedRuntime } from "./runtime";\n\nawait registerBazisGeneratedRuntime();\n');
516
502
  }
517
503
  for (const warning of warnings) console.warn(`[di:generate] WARNING: ${warning}`);
518
- console.log(`[di:generate] target=${name} sources=${sourceEntries.length} deps=${classDeps.size} controllers=${Object.keys(httpBindings).length} requestModels=${requestModelImports.length} listModels=${listModelImports.length} agents=${agent.agentCount} prepassMs=${prepassMs.toFixed(1)} analysisMs=${analysisMs.toFixed(1)} totalMs=${(performance.now() - startedAt).toFixed(1)}`);
504
+ console.log(`[di:generate] target=${name} sources=${sourceEntries.length} deps=${classDeps.size} controllers=${Object.keys(httpBindings).length} requestModels=${requestModelImports.length} listModels=${listModelImports.length} tools=${tools.toolCount} prepassMs=${prepassMs.toFixed(1)} analysisMs=${analysisMs.toFixed(1)} totalMs=${(performance.now() - startedAt).toFixed(1)}`);
519
505
  return {
520
506
  analysisMs,
521
507
  renderMs: performance.now() - renderStartedAt,
522
508
  eligible: sourceEntries.length,
523
- candidates: classDeps.size + Object.keys(httpBindings).length + requestModelImports.length + listModelImports.length + agent.agentCount,
509
+ candidates: classDeps.size + Object.keys(httpBindings).length + requestModelImports.length + listModelImports.length + tools.toolCount,
524
510
  };
525
511
  }
526
512
 
527
- function classifyAgentWarnings(agentWarnings: readonly string[]): void {
528
- for (let index = 0; index < agentWarnings.length; index += 1) {
529
- const warning = agentWarnings[index] as string;
513
+ function classifyToolWarnings(toolWarnings: readonly string[]): void {
514
+ for (let index = 0; index < toolWarnings.length; index += 1) {
515
+ const warning = toolWarnings[index] as string;
530
516
  if (warning.includes("ambiguous") || warning.includes("declared in more than one file")) {
531
517
  fatalErrors.push(warning);
532
518
  } else {
@@ -561,8 +547,8 @@ function renderGeneratedRuntime(targetId: string): string {
561
547
  "}",
562
548
  "",
563
549
  "async function loadTarget(): Promise<void> {",
564
- " const [deps, bindings, requests, lists, openApi, agentCatalog] = await Promise.all([import(\"./deps\"), import(\"./bindings\"), import(\"./httpRequestModels\"), import(\"./httpListModels\"), import(\"./openapi\"), import(\"./agentCatalog\")]);",
565
- ` await registerBazisGeneratedTargetDescriptor({ id: ${JSON.stringify(targetId)}, classDeps: deps.GENERATED_TARGET_CLASS_DEPS, bindings: bindings.GENERATED_TARGET_BINDINGS, providerAttachments: bindings.GENERATED_PROVIDER_ATTACHMENTS, requestModels: requests.GENERATED_REQUEST_MODELS, requestShapes: requests.GENERATED_REQUEST_MODEL_SHAPES, listModels: lists.GENERATED_LIST_MODELS, openApi: openApi.GENERATED_OPENAPI_METADATA, openApiSchemaModels: openApi.GENERATED_OPENAPI_SCHEMA_MODELS, agentMetadata: agentCatalog.GENERATED_AGENT_METADATA });`,
550
+ " const [deps, bindings, requests, lists, openApi] = await Promise.all([import(\"./deps\"), import(\"./bindings\"), import(\"./httpRequestModels\"), import(\"./httpListModels\"), import(\"./openapi\")]);",
551
+ ` await registerBazisGeneratedTargetDescriptor({ id: ${JSON.stringify(targetId)}, classDeps: deps.GENERATED_TARGET_CLASS_DEPS, bindings: bindings.GENERATED_TARGET_BINDINGS, providerAttachments: bindings.GENERATED_PROVIDER_ATTACHMENTS, requestModels: requests.GENERATED_REQUEST_MODELS, requestShapes: requests.GENERATED_REQUEST_MODEL_SHAPES, listModels: lists.GENERATED_LIST_MODELS, openApi: openApi.GENERATED_OPENAPI_METADATA, openApiSchemaModels: openApi.GENERATED_OPENAPI_SCHEMA_MODELS });`,
566
552
  "}",
567
553
  "",
568
554
  ].join("\n");
@@ -582,8 +568,8 @@ function renderGeneratedRuntime(targetId: string): string {
582
568
  " // Test-only owner-private hook: it is inert unless an isolated test installs it before loading this generated module.",
583
569
  ' (globalThis as { __bazisGeneratedRuntimeTestHook?: (target: string) => void }).__bazisGeneratedRuntimeTestHook?.("production");',
584
570
  ' const { registerBazisGeneratedTargetDescriptor, warnIfGeneratedSourcesChanged } = await import("bazis/core/generatedRuntime");',
585
- " const [deps, bindings, requests, lists, openApi, agentCatalog] = await Promise.all([import(\"./deps\"), import(\"./bindings\"), import(\"./httpRequestModels\"), import(\"./httpListModels\"), import(\"./openapi\"), import(\"./agentCatalog\")]);",
586
- ` await registerBazisGeneratedTargetDescriptor({ id: ${JSON.stringify(targetId)}, classDeps: deps.GENERATED_TARGET_CLASS_DEPS, bindings: bindings.GENERATED_TARGET_BINDINGS, providerAttachments: bindings.GENERATED_PROVIDER_ATTACHMENTS, requestModels: requests.GENERATED_REQUEST_MODELS, requestShapes: requests.GENERATED_REQUEST_MODEL_SHAPES, listModels: lists.GENERATED_LIST_MODELS, openApi: openApi.GENERATED_OPENAPI_METADATA, openApiSchemaModels: openApi.GENERATED_OPENAPI_SCHEMA_MODELS, agentMetadata: agentCatalog.GENERATED_AGENT_METADATA });`,
571
+ " const [deps, bindings, requests, lists, openApi] = await Promise.all([import(\"./deps\"), import(\"./bindings\"), import(\"./httpRequestModels\"), import(\"./httpListModels\"), import(\"./openapi\")]);",
572
+ ` await registerBazisGeneratedTargetDescriptor({ id: ${JSON.stringify(targetId)}, classDeps: deps.GENERATED_TARGET_CLASS_DEPS, bindings: bindings.GENERATED_TARGET_BINDINGS, providerAttachments: bindings.GENERATED_PROVIDER_ATTACHMENTS, requestModels: requests.GENERATED_REQUEST_MODELS, requestShapes: requests.GENERATED_REQUEST_MODEL_SHAPES, listModels: lists.GENERATED_LIST_MODELS, openApi: openApi.GENERATED_OPENAPI_METADATA, openApiSchemaModels: openApi.GENERATED_OPENAPI_SCHEMA_MODELS });`,
587
573
  " // Started from sources edited after codegen: say so instead of silently missing routes.",
588
574
  ' warnIfGeneratedSourcesChanged(import.meta.dir, (await import("./fingerprint")).GENERATED_SOURCE_FINGERPRINT);',
589
575
  "}",
@@ -1348,11 +1334,11 @@ function resolveUiProfileResponseModelImports(): OpenApiSchemaModelImport[] {
1348
1334
  || sourcePathForDeclaration(left.declaration).localeCompare(sourcePathForDeclaration(right.declaration)));
1349
1335
  }
1350
1336
 
1351
- /** Agent DTOs and their referenced classes share the existing nominal OpenAPI registry. */
1352
- function resolveOpenApiSchemaModelImports(agentModels: readonly ts.ClassDeclaration[], agentNames: readonly string[]): OpenApiSchemaModelImport[] {
1337
+ /** Tool contracts and their referenced classes share the existing nominal OpenAPI registry. */
1338
+ function resolveOpenApiSchemaModelImports(toolModels: readonly ts.ClassDeclaration[], toolNames: readonly string[]): OpenApiSchemaModelImport[] {
1353
1339
  const models = new Map(resolveUiProfileResponseModelImports().map((model) => [model.declaration, model.schemaName]));
1354
1340
  // Empty class contracts still have a real object schema and must survive filtering.
1355
- for (const declaration of agentModels) {
1341
+ for (const declaration of toolModels) {
1356
1342
  const name = openApiCodegen.schemaNameForDeclaration(declaration.name!.text, declaration);
1357
1343
  openApiSchemas[name] ??= openApiCodegen.schemaFromDeclaration(declaration);
1358
1344
  models.set(declaration, name);
@@ -1363,11 +1349,11 @@ function resolveOpenApiSchemaModelImports(agentModels: readonly ts.ClassDeclarat
1363
1349
  reachable.add(name);
1364
1350
  collectSchemaRefs(openApiSchemas[name], add);
1365
1351
  };
1366
- for (const name of agentNames) add(name);
1352
+ for (const name of toolNames) add(name);
1367
1353
  for (const [declaration, name] of openApiSchemaModels) {
1368
1354
  if (!reachable.has(name)) continue;
1369
1355
  if (!isNamedExportedTopLevelClass(declaration)) {
1370
- fatalErrors.push(`BAZIS_AGENT_SCHEMA_MODEL_UNIMPORTABLE: Agent schema ${name} must be a named exported top-level class (${sourcePathForDeclaration(declaration)}).`);
1356
+ fatalErrors.push(`BAZIS_TOOL_SCHEMA_MODEL_UNIMPORTABLE: Tool schema ${name} must be a named exported top-level class (${sourcePathForDeclaration(declaration)}).`);
1371
1357
  continue;
1372
1358
  }
1373
1359
  openApiSchemas[name] ??= openApiCodegen.schemaFromDeclaration(declaration);
@@ -0,0 +1,122 @@
1
+ import ts from "typescript";
2
+
3
+ export interface ToolCodegenSource {
4
+ readonly filePath: string;
5
+ /** Program-owned AST; codegen never reparses application source. */
6
+ readonly sourceFile?: ts.SourceFile;
7
+ /** Test fixture input; project generation always supplies sourceFile. */
8
+ readonly content?: string;
9
+ }
10
+
11
+ export interface ToolCodegenResult {
12
+ readonly warnings: readonly string[];
13
+ readonly toolCount: number;
14
+ /** `input`/`output` classes of `@Tool` declarations: they get JSON schemas in the OpenAPI registry. */
15
+ readonly schemaNames: readonly string[];
16
+ /** Program-owned declarations of those classes, for nominal schema bindings. */
17
+ readonly schemaModels: readonly ts.ClassDeclaration[];
18
+ }
19
+
20
+ export interface ToolCodegenOptions {
21
+ /** Use the same naming policy as the project's OpenAPI analyzer. */
22
+ readonly schemaNameForDeclaration?: (name: string, declaration: ts.ClassDeclaration) => string;
23
+ }
24
+
25
+ /**
26
+ * Finds `@Tool({ input, output })` classes. Tool metadata itself is read at
27
+ * runtime from the decorator; codegen only makes sure the contract classes
28
+ * have generated JSON schemas for the model and for validation.
29
+ */
30
+ export function collectToolSchemaModels(
31
+ sources: readonly ToolCodegenSource[],
32
+ options: ToolCodegenOptions = {},
33
+ ): ToolCodegenResult {
34
+ const warnings: string[] = [];
35
+ const declarations = new Map<string, ts.ClassDeclaration>();
36
+ const files = new Map<string, string>();
37
+ const ambiguous = new Set<string>();
38
+ const parsed = sources.map((source) =>
39
+ source.sourceFile ?? ts.createSourceFile(source.filePath, source.content ?? "", ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS));
40
+
41
+ for (const source of parsed) {
42
+ visitClasses(source, (node) => {
43
+ const name = node.name!.text;
44
+ const existing = files.get(name);
45
+ if (existing === undefined) files.set(name, source.fileName);
46
+ else if (existing !== source.fileName) ambiguous.add(name);
47
+ declarations.set(name, node);
48
+ });
49
+ }
50
+
51
+ const contractNames = new Set<string>();
52
+ let toolCount = 0;
53
+ for (const source of parsed) {
54
+ visitClasses(source, (node) => {
55
+ const call = toolDecoratorCall(node);
56
+ if (call === undefined) return;
57
+ const className = node.name!.text;
58
+ const where = sourceLocation(source, node);
59
+ if (!isNamedExportedClass(node)) {
60
+ warnings.push(`@Tool class "${className}" (${where}) is not a named export; codegen can only import named-export classes.`);
61
+ return;
62
+ }
63
+ toolCount += 1;
64
+ const options = call.arguments[0];
65
+ if (options === undefined || !ts.isObjectLiteralExpression(options)) return;
66
+ for (const property of options.properties) {
67
+ if (!ts.isPropertyAssignment(property)) continue;
68
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name) ? property.name.text : undefined;
69
+ if (key !== "input" && key !== "output") continue;
70
+ if (!ts.isIdentifier(property.initializer)) {
71
+ warnings.push(`@Tool ${className} (${where}): ${key} must be a class identifier.`);
72
+ continue;
73
+ }
74
+ const contract = property.initializer.text;
75
+ if (ambiguous.has(contract)) {
76
+ warnings.push(`@Tool ${className} (${where}): ${key} class "${contract}" is declared in more than one file.`);
77
+ continue;
78
+ }
79
+ contractNames.add(contract);
80
+ }
81
+ });
82
+ }
83
+
84
+ const schemaModels = [...contractNames]
85
+ .map((name) => declarations.get(name))
86
+ .filter((declaration): declaration is ts.ClassDeclaration => declaration !== undefined);
87
+ return {
88
+ warnings: Object.freeze(warnings),
89
+ toolCount,
90
+ schemaNames: Object.freeze(schemaModels
91
+ .map((declaration) => options.schemaNameForDeclaration?.(declaration.name!.text, declaration) ?? declaration.name!.text)
92
+ .sort((left, right) => left.localeCompare(right))),
93
+ schemaModels: Object.freeze(schemaModels),
94
+ };
95
+ }
96
+
97
+ function visitClasses(source: ts.SourceFile, onClass: (node: ts.ClassDeclaration) => void): void {
98
+ const visit = (node: ts.Node): void => {
99
+ if (ts.isClassDeclaration(node) && node.name) onClass(node);
100
+ ts.forEachChild(node, visit);
101
+ };
102
+ ts.forEachChild(source, visit);
103
+ }
104
+
105
+ function toolDecoratorCall(node: ts.ClassDeclaration): ts.CallExpression | undefined {
106
+ for (const decorator of ts.getDecorators(node) ?? []) {
107
+ const expression = decorator.expression;
108
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "Tool") return expression;
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ function isNamedExportedClass(node: ts.ClassDeclaration): boolean {
114
+ const modifiers = ts.getModifiers(node) ?? [];
115
+ return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
116
+ && !modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
117
+ }
118
+
119
+ function sourceLocation(source: ts.SourceFile, node: ts.Node): string {
120
+ const position = source.getLineAndCharacterOfPosition(node.getStart(source));
121
+ return `${source.fileName}:${position.line + 1}:${position.character + 1}`;
122
+ }