bazis 0.138.0 → 0.140.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 (44) hide show
  1. package/README.md +13 -5
  2. package/core/access/Access.module.ts +6 -1
  3. package/core/access/catalog/AccessCatalog.ts +56 -7
  4. package/core/access/catalog/IAccessCatalog.service.ts +28 -2
  5. package/core/access/catalog/PermissionSourceLoader.ts +43 -0
  6. package/core/access/catalog/PermissionSources.ts +222 -0
  7. package/core/access/catalog/moduleFields.ts +60 -6
  8. package/core/access/catalog/permissions.ts +46 -1
  9. package/core/access/check/Access.ts +55 -28
  10. package/core/access/check/accessConfig.ts +9 -1
  11. package/core/access/check/can.ts +2 -2
  12. package/core/access/check/conditions.ts +69 -11
  13. package/core/access/check/decimal.ts +61 -0
  14. package/core/access/check/filter.ts +39 -8
  15. package/core/access/contracts/PermissionSource.ts +32 -0
  16. package/core/access/contracts/types.ts +12 -2
  17. package/core/access/generated/deps.ts +5 -1
  18. package/core/access/http/AccessCatalog.controller.ts +9 -2
  19. package/core/access/index.ts +12 -1
  20. package/core/agent/AgentContextBuilder.ts +119 -16
  21. package/core/agent/AgentRuntime.ts +8 -0
  22. package/core/agent/AgentToolExecutor.ts +7 -0
  23. package/core/agent/ModelProvider.ts +6 -0
  24. package/core/agent/README.md +35 -3
  25. package/core/agent/errors.ts +32 -0
  26. package/core/agent/index.ts +7 -0
  27. package/core/agent/internal/AgentExecutionDriver.ts +598 -24
  28. package/core/agent/internal/AgentExecutionState.ts +26 -3
  29. package/core/agent/semantic.ts +60 -1
  30. package/core/cache/SPEC.md +3 -3
  31. package/core/di/README.md +10 -3
  32. package/core/infra/anthropic.ts +11 -0
  33. package/core/infra/connectors/anthropic.ts +440 -0
  34. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  35. package/core/infra/connectors/llm.ts +12 -0
  36. package/core/infra/connectors/llmRouter.ts +12 -0
  37. package/core/infra/connectors/openaiCompatible.ts +128 -63
  38. package/core/infra/connectors/openaiTextStream.ts +5 -2
  39. package/core/infra/connectors/toolNames.ts +49 -0
  40. package/core/scripts/di-generate.ts +34 -4
  41. package/core/websocket/DESIGN.md +2 -2
  42. package/index.ts +5 -0
  43. package/library/orm/Repository/README.md +14 -11
  44. package/package.json +7 -2
@@ -1,5 +1,7 @@
1
1
  import {
2
+ AgentModelContextOverflowError,
2
3
  AgentModelTimeoutError,
4
+ AgentModelUnavailableError,
3
5
  agentMessage,
4
6
  agentModelResponse,
5
7
  agentToolCall,
@@ -22,6 +24,7 @@ import {
22
24
  type LlmProviderAdapter,
23
25
  } from "./llm";
24
26
  import { readOpenAiTextStream } from "./openaiTextStream";
27
+ import { providerToolName, toolNameMapping, type ToolNameMapping } from "./toolNames";
25
28
  import { InfraError } from "../InfraConnector";
26
29
  import { redactSensitiveText } from "../../../library/redaction";
27
30
 
@@ -52,9 +55,14 @@ export class OpenAiCompatibleProviderError extends Error {
52
55
 
53
56
  type OpenAiChatRole = "system" | "user" | "assistant" | "tool";
54
57
 
58
+ type OpenAiContentPart =
59
+ | { readonly type: "text"; readonly text: string }
60
+ | { readonly type: "image_url"; readonly image_url: { readonly url: string; readonly detail: "auto" | "low" | "high" } }
61
+ | { readonly type: "file"; readonly file: { readonly file_data: string; readonly filename: string } };
62
+
55
63
  interface OpenAiChatMessage {
56
64
  readonly role: OpenAiChatRole;
57
- readonly content?: string | null;
65
+ readonly content?: string | readonly OpenAiContentPart[] | null;
58
66
  readonly tool_call_id?: string;
59
67
  readonly tool_calls?: readonly OpenAiToolCall[];
60
68
  }
@@ -92,17 +100,14 @@ interface OpenAiChatResponse {
92
100
  readonly prompt_tokens?: number;
93
101
  readonly completion_tokens?: number;
94
102
  readonly total_tokens?: number;
103
+ readonly prompt_tokens_details?: { readonly cached_tokens?: number } | null;
104
+ readonly completion_tokens_details?: { readonly reasoning_tokens?: number } | null;
95
105
  };
96
106
  readonly error?: {
97
107
  readonly message?: string;
98
108
  };
99
109
  }
100
110
 
101
- interface ToolNameMapping {
102
- readonly providerToBazis: ReadonlyMap<string, string>;
103
- readonly bazisToProvider: ReadonlyMap<string, string>;
104
- }
105
-
106
111
  function stripTrailingSlash(value: string): string {
107
112
  return value.replace(/\/+$/, "");
108
113
  }
@@ -126,47 +131,6 @@ function cancelBodyBestEffort(body: ReadableStream<Uint8Array> | null | undefine
126
131
  void body.cancel(reason).catch(() => undefined);
127
132
  }
128
133
 
129
- function shortHash(value: string): string {
130
- let hash = 2166136261;
131
- for (let index = 0; index < value.length; index += 1) {
132
- hash ^= value.charCodeAt(index);
133
- hash = Math.imul(hash, 16777619);
134
- }
135
- return (hash >>> 0).toString(36);
136
- }
137
-
138
- function providerToolName(name: string, used: Set<string>): string {
139
- const replaced = name.replace(/[^a-zA-Z0-9_-]/g, "_");
140
- const prefixed = /^[a-zA-Z_]/.test(replaced) ? replaced : `tool_${replaced}`;
141
- const trimmed = prefixed.length <= 64 ? prefixed : `${prefixed.slice(0, 55)}_${shortHash(name)}`;
142
- if (!used.has(trimmed)) {
143
- used.add(trimmed);
144
- return trimmed;
145
- }
146
-
147
- const suffix = shortHash(name);
148
- const base = trimmed.slice(0, Math.max(1, 63 - suffix.length));
149
- const unique = `${base}_${suffix}`;
150
- if (used.has(unique)) {
151
- throw new OpenAiCompatibleProviderError(`Tool name "${name}" collides after OpenAI-compatible normalization.`);
152
- }
153
- used.add(unique);
154
- return unique;
155
- }
156
-
157
- function toolNameMapping(tools: readonly AgentToolContract[]): ToolNameMapping {
158
- const providerToBazis = new Map<string, string>();
159
- const bazisToProvider = new Map<string, string>();
160
- const used = new Set<string>();
161
- for (let index = 0; index < tools.length; index += 1) {
162
- const tool = tools[index] as AgentToolContract;
163
- const providerName = providerToolName(tool.name, used);
164
- providerToBazis.set(providerName, tool.name);
165
- bazisToProvider.set(tool.name, providerName);
166
- }
167
- return Object.freeze({ providerToBazis, bazisToProvider });
168
- }
169
-
170
134
  function schemaForTool(tool: AgentToolContract): JsonObject {
171
135
  if (tool.input?.kind === "json-schema") {
172
136
  return tool.input.schema;
@@ -227,6 +191,28 @@ function contentText(parts: readonly AgentContentPart[]): string {
227
191
  return lines.join("\n");
228
192
  }
229
193
 
194
+ /**
195
+ * What a user sends as the provider expects it: one string for words, parts
196
+ * when there is an image or a file whose bytes are in the dialog. A file given
197
+ * by address is named in the text: Chat Completions takes file content only.
198
+ */
199
+ function userContent(parts: readonly AgentContentPart[]): string | readonly OpenAiContentPart[] {
200
+ const inline = (part: AgentContentPart): boolean => part.kind === "image" || (part.kind === "file" && part.uri.startsWith("data:"));
201
+ if (!parts.some(inline)) return contentText(parts);
202
+ const out: OpenAiContentPart[] = [];
203
+ for (const part of parts) {
204
+ if (part.kind === "image") {
205
+ out.push({ type: "image_url", image_url: { url: part.uri, detail: part.detail } });
206
+ } else if (part.kind === "file" && part.uri.startsWith("data:")) {
207
+ out.push({ type: "file", file: { file_data: part.uri, filename: part.name ?? "file" } });
208
+ } else {
209
+ const text = contentText([part]);
210
+ if (text.length > 0) out.push({ type: "text", text });
211
+ }
212
+ }
213
+ return Object.freeze(out);
214
+ }
215
+
230
216
  function toOpenAiRole(role: AgentMessage["role"]): OpenAiChatRole {
231
217
  if (role === "developer") {
232
218
  return "system";
@@ -266,17 +252,75 @@ function finishReasonOf(reason: string | undefined, hasToolCalls: boolean): Agen
266
252
  return "error";
267
253
  }
268
254
 
269
- function parseToolArguments(raw: string, callId: string): JsonValue {
255
+ /** Arguments the model wrote: the value, or why they could not be read. */
256
+ function parseToolArguments(raw: string, callId: string): { readonly input: JsonValue } | { readonly inputError: string } {
270
257
  if (raw.trim().length === 0) {
271
- return Object.freeze({});
258
+ return { input: Object.freeze({}) };
272
259
  }
273
260
  try {
274
- return normalizeJsonValue(JSON.parse(raw), `toolCall.${callId}.arguments`);
261
+ return { input: normalizeJsonValue(JSON.parse(raw), `toolCall.${callId}.arguments`) };
275
262
  } catch (error) {
276
- throw new OpenAiCompatibleProviderError(`OpenAI-compatible tool arguments for "${callId}" are invalid JSON: ${errorMessageOf(error)}`);
263
+ // Not a provider failure: the model gets the reason as a Tool error and writes the call again.
264
+ return { inputError: errorMessageOf(error) };
265
+ }
266
+ }
267
+
268
+ /** HTTP statuses that mean "not now": the runtime repeats the request. */
269
+ function isUnavailableStatus(status: number): boolean {
270
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
271
+ }
272
+
273
+ const MAX_ERROR_BODY_BYTES = 64 * 1024;
274
+
275
+ /**
276
+ * Whether an error body says the request is longer than the model's window.
277
+ * The body is only classified: its text may repeat the prompt and never
278
+ * leaves this function.
279
+ */
280
+ async function saysContextOverflow(response: Response, signal: AbortSignal): Promise<boolean> {
281
+ if (response.body === null) return false;
282
+ const reader = response.body.getReader();
283
+ const chunks: Uint8Array[] = [];
284
+ let total = 0;
285
+ const cancel = (): void => { void reader.cancel().catch(() => undefined); };
286
+ signal.addEventListener("abort", cancel, { once: true });
287
+ try {
288
+ while (total < MAX_ERROR_BODY_BYTES) {
289
+ const item = await reader.read();
290
+ if (item.done) break;
291
+ chunks.push(item.value);
292
+ total += item.value.byteLength;
293
+ }
294
+ const bytes = new Uint8Array(total);
295
+ let offset = 0;
296
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
297
+ const text = new TextDecoder().decode(bytes.subarray(0, MAX_ERROR_BODY_BYTES));
298
+ let code: unknown;
299
+ let message: unknown = text;
300
+ try {
301
+ const error = (JSON.parse(text) as { error?: { code?: unknown; message?: unknown } }).error;
302
+ code = error?.code;
303
+ if (typeof error?.message === "string") message = error.message;
304
+ } catch { /* Not JSON: the plain text is looked at. */ }
305
+ return code === "context_length_exceeded"
306
+ || (typeof message === "string" && /context[ _-]?length|context window|maximum context|prompt is too long|too many (input )?tokens|input is too long/i.test(message));
307
+ } catch {
308
+ return false;
309
+ } finally {
310
+ signal.removeEventListener("abort", cancel);
311
+ cancel();
277
312
  }
278
313
  }
279
314
 
315
+ /** `Retry-After`: seconds or an HTTP date. */
316
+ function retryAfterMsOf(header: string | null): number | undefined {
317
+ if (header === null) return undefined;
318
+ const text = header.trim();
319
+ if (/^\d+(\.\d+)?$/.test(text)) return Math.round(Number(text) * 1000);
320
+ const date = Date.parse(text);
321
+ return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
322
+ }
323
+
280
324
  function normalizeToolCalls(
281
325
  calls: readonly OpenAiToolCall[] | undefined,
282
326
  mapping: ToolNameMapping,
@@ -298,7 +342,7 @@ function normalizeToolCalls(
298
342
  out.push(agentToolCall({
299
343
  id: call.id,
300
344
  name,
301
- input: parseToolArguments(call.function?.arguments ?? "", call.id),
345
+ ...parseToolArguments(call.function?.arguments ?? "", call.id),
302
346
  }));
303
347
  }
304
348
  return Object.freeze(out);
@@ -312,6 +356,8 @@ function buildUsage(usage: OpenAiChatResponse["usage"]): AgentModelResponse["usa
312
356
  ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),
313
357
  ...(usage.completion_tokens !== undefined ? { outputTokens: usage.completion_tokens } : {}),
314
358
  ...(usage.total_tokens !== undefined ? { totalTokens: usage.total_tokens } : {}),
359
+ ...(typeof usage.prompt_tokens_details?.cached_tokens === "number" ? { cacheReadTokens: usage.prompt_tokens_details.cached_tokens } : {}),
360
+ ...(typeof usage.completion_tokens_details?.reasoning_tokens === "number" ? { reasoningTokens: usage.completion_tokens_details.reasoning_tokens } : {}),
315
361
  });
316
362
  }
317
363
 
@@ -401,6 +447,11 @@ export class OpenAiCompatibleModelProvider implements AgentModelProvider {
401
447
  }
402
448
  }
403
449
 
450
+ /** The context window the application stated for this model (`contextTokens`). */
451
+ public contextTokens(): number | undefined {
452
+ return this.connection.contextTokens;
453
+ }
454
+
404
455
  /** Base URL of the provider, for diagnostics. */
405
456
  public get address(): string {
406
457
  return this.baseUrl;
@@ -490,9 +541,10 @@ export class OpenAiCompatibleModelProvider implements AgentModelProvider {
490
541
  continue;
491
542
  }
492
543
 
544
+ const role = toOpenAiRole(message.role);
493
545
  out.push(Object.freeze({
494
- role: toOpenAiRole(message.role),
495
- content: contentText(message.content),
546
+ role,
547
+ content: role === "user" ? userContent(message.content) : contentText(message.content),
496
548
  }));
497
549
  }
498
550
  return Object.freeze(out);
@@ -507,19 +559,32 @@ export class OpenAiCompatibleModelProvider implements AgentModelProvider {
507
559
  : setTimeout(() => abort.abort(new AgentModelTimeoutError("OpenAI-compatible request timed out.")), this.requestTimeoutMs);
508
560
  try {
509
561
  abort.signal.throwIfAborted();
510
- const response = await this.fetcher(`${this.baseUrl}${path}`, {
511
- method,
512
- headers: this.headers,
513
- body: body === undefined ? undefined : JSON.stringify(body),
514
- signal: abort.signal,
515
- });
562
+ let response: Response;
563
+ try {
564
+ response = await this.fetcher(`${this.baseUrl}${path}`, {
565
+ method,
566
+ headers: this.headers,
567
+ body: body === undefined ? undefined : JSON.stringify(body),
568
+ signal: abort.signal,
569
+ });
570
+ } catch (error) {
571
+ // A cancelled or timed-out request keeps its own reason; a broken connection can be repeated.
572
+ if (abort.signal.aborted) throw error;
573
+ throw new AgentModelUnavailableError(`OpenAI-compatible ${method} ${path} could not reach the provider: ${errorMessageOf(error)}`);
574
+ }
516
575
  if (!response.ok) {
576
+ const message = `OpenAI-compatible ${method} ${path} failed with HTTP ${response.status}.`;
577
+ // A refusal of a chat request may mean "too long": the runtime answers that by compacting the dialog.
578
+ if (method === "POST" && (response.status === 400 || response.status === 413 || response.status === 422) && await saysContextOverflow(response, abort.signal)) {
579
+ throw new AgentModelContextOverflowError(`${message} The request does not fit the model's context window.`);
580
+ }
517
581
  // Cancel the provider-controlled body without exposing it: error bodies
518
582
  // may echo prompts, credentials or tenant data.
519
583
  cancelBodyBestEffort(response.body);
520
- throw new OpenAiCompatibleProviderError(
521
- `OpenAI-compatible ${method} ${path} failed with HTTP ${response.status}.`,
522
- );
584
+ if (isUnavailableStatus(response.status)) {
585
+ throw new AgentModelUnavailableError(message, { retryAfterMs: retryAfterMsOf(response.headers.get("retry-after")) });
586
+ }
587
+ throw new OpenAiCompatibleProviderError(message);
523
588
  }
524
589
  if (response.status === 204) {
525
590
  return {};
@@ -121,10 +121,13 @@ export async function readOpenAiTextStream(
121
121
  // EOF alone is not success: both a terminal choice and [DONE] are required.
122
122
  if (!done) fail("was interrupted before completion");
123
123
  const toolCalls = [...calls.entries()].sort(([a], [b]) => a - b).map(([index, call], position) => {
124
- if (index !== position || !call.id || !call.function.name || !call.function.arguments) fail("contains an incomplete tool call");
124
+ if (index !== position || !call.id || !call.function.name) fail("contains an incomplete tool call");
125
125
  return call;
126
126
  });
127
- if ((toolCalls.length > 0) !== (finish === "tool_calls") || new Set(toolCalls.map(call => call.id)).size !== toolCalls.length) fail("contains inconsistent tool calls");
127
+ // "length" with tool calls: the output limit cut the arguments. The calls are
128
+ // kept, so the model hears that the arguments are broken and writes them again.
129
+ const cut = finish === "length" && toolCalls.length > 0;
130
+ if ((!cut && (toolCalls.length > 0) !== (finish === "tool_calls")) || new Set(toolCalls.map(call => call.id)).size !== toolCalls.length) fail("contains inconsistent tool calls");
128
131
  return { choices: [{ finish_reason: finish, message: { content: text.join(""), ...(toolCalls.length ? { tool_calls: toolCalls } : {}) } }], usage };
129
132
  } finally {
130
133
  signal.removeEventListener("abort", cancel);
@@ -0,0 +1,49 @@
1
+ import type { AgentToolContract } from "../../agent";
2
+
3
+ export interface ToolNameMapping {
4
+ readonly providerToBazis: ReadonlyMap<string, string>;
5
+ readonly bazisToProvider: ReadonlyMap<string, string>;
6
+ }
7
+
8
+ function shortHash(value: string): string {
9
+ let hash = 2166136261;
10
+ for (let index = 0; index < value.length; index += 1) {
11
+ hash ^= value.charCodeAt(index);
12
+ hash = Math.imul(hash, 16777619);
13
+ }
14
+ return (hash >>> 0).toString(36);
15
+ }
16
+
17
+ /** A Tool name as providers accept it: Latin letters, digits, `_` and `-`, at most 64 characters, unique in a request. */
18
+ export function providerToolName(name: string, used: Set<string>): string {
19
+ const replaced = name.replace(/[^a-zA-Z0-9_-]/g, "_");
20
+ const prefixed = /^[a-zA-Z_]/.test(replaced) ? replaced : `tool_${replaced}`;
21
+ const trimmed = prefixed.length <= 64 ? prefixed : `${prefixed.slice(0, 55)}_${shortHash(name)}`;
22
+ if (!used.has(trimmed)) {
23
+ used.add(trimmed);
24
+ return trimmed;
25
+ }
26
+
27
+ const suffix = shortHash(name);
28
+ const base = trimmed.slice(0, Math.max(1, 63 - suffix.length));
29
+ const unique = `${base}_${suffix}`;
30
+ if (used.has(unique)) {
31
+ throw new Error(`Tool name "${name}" collides with another Tool after its name is made acceptable to the model provider.`);
32
+ }
33
+ used.add(unique);
34
+ return unique;
35
+ }
36
+
37
+ /** Names of a request's Tools as the provider sees them, and the way back. */
38
+ export function toolNameMapping(tools: readonly AgentToolContract[]): ToolNameMapping {
39
+ const providerToBazis = new Map<string, string>();
40
+ const bazisToProvider = new Map<string, string>();
41
+ const used = new Set<string>();
42
+ for (let index = 0; index < tools.length; index += 1) {
43
+ const tool = tools[index] as AgentToolContract;
44
+ const providerName = providerToolName(tool.name, used);
45
+ providerToBazis.set(providerName, tool.name);
46
+ bazisToProvider.set(tool.name, providerName);
47
+ }
48
+ return Object.freeze({ providerToBazis, bazisToProvider });
49
+ }
@@ -191,6 +191,8 @@ interface ConstructorDependencies {
191
191
  let classDeps = new Map<ts.ClassDeclaration, readonly CollectedDependency[]>();
192
192
  /** `permissions` of the target's modules: the exported constant and its file. */
193
193
  let permissionDeclarations = new Map<string, { readonly name: string; readonly file: string }>();
194
+ /** The prefixes of `permissionSources` of the target's modules: any key under one is a permission key. */
195
+ let permissionSourcePrefixes = new Set<string>();
194
196
  let inferredDiClasses = new Set<ts.ClassDeclaration>();
195
197
  let explicitlyBoundDiClasses = new Set<ts.ClassDeclaration>();
196
198
  // Known runtime token names: createToken descriptions + class names usable as class-tokens.
@@ -507,7 +509,7 @@ function collectDynamicImportDiagnostics(source: ts.SourceFile, target: string,
507
509
 
508
510
  function resetTargetAnalysis(files: Set<string>, generatedDir: string): void {
509
511
  activeGeneratedDir = generatedDir;
510
- classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set(); permissionDeclarations = new Map();
512
+ classDeps = new Map(); inferredDiClasses = new Set(); explicitlyBoundDiClasses = new Set(); permissionDeclarations = new Map(); permissionSourcePrefixes = new Set();
511
513
  tokenDescriptions = new Set(); configPrefixesByType = new Map(); knownClassNames = new Set(); warnings = []; fatalErrors = [];
512
514
  httpBindings = {}; httpControllerFiles = {}; openApiSchemas = {}; openApiOperations = {};
513
515
  grpcBindings = new Map();
@@ -557,7 +559,7 @@ async function generateTarget(name: string, reachable: Set<string>, production:
557
559
  stageWrite(`${generatedDir}/httpRequestModels.ts`, requestModels);
558
560
  stageWrite(`${generatedDir}/httpListModels.ts`, listModels);
559
561
  stageWrite(`${generatedDir}/openapi.ts`, openApi);
560
- if (permissionDeclarations.size > 0) stageWrite(`${generatedDir}/permissions.ts`, renderPermissions());
562
+ if (permissionDeclarations.size > 0 || permissionSourcePrefixes.size > 0) stageWrite(`${generatedDir}/permissions.ts`, renderPermissions());
561
563
  else stageRemove(`${generatedDir}/permissions.ts`);
562
564
  // Generated before 0.99.0, when agents were classes; nothing imports it now.
563
565
  stageRemove(`${generatedDir}/agentCatalog.ts`);
@@ -715,9 +717,17 @@ function renderGeneratedRuntime(targetId: string): string {
715
717
  /** Types only: the keys of the target's permissions for `can`, `access.can`, `access.filter` and `defineRoles`. */
716
718
  function renderPermissions(): string {
717
719
  const entries = [...permissionDeclarations.values()].sort((left, right) => left.file.localeCompare(right.file) || left.name.localeCompare(right.name));
718
- const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually.", 'import type { PermissionsOf } from "bazis/core/access";'];
720
+ const lines = ["// This file is auto-generated by `bazis codegen`.", "// Do not edit manually."];
721
+ // Without an import or an export the file would be a script, and `declare module` would replace the module instead of adding to it.
722
+ lines.push(entries.length > 0 ? 'import type { PermissionsOf } from "bazis/core/access";' : "export {};");
719
723
  entries.forEach((entry, index) => lines.push(`import type { ${entry.name} as Permissions_${index} } from ${JSON.stringify(toModuleSpecifierFrom(activeGeneratedDir, entry.file))};`));
720
- lines.push("", 'declare module "bazis/core/access" {', ` interface BazisPermissions extends ${entries.map((_, index) => `PermissionsOf<typeof Permissions_${index}>`).join(", ")} {}`, "}", "");
724
+ lines.push("", 'declare module "bazis/core/access" {');
725
+ if (entries.length > 0) lines.push(` interface BazisPermissions extends ${entries.map((_, index) => `PermissionsOf<typeof Permissions_${index}>`).join(", ")} {}`);
726
+ // The prefixes of `permissionSources`: `${prefix}.${string}` is a permission key too (ACCESS-001 §3.1).
727
+ if (permissionSourcePrefixes.size > 0) {
728
+ lines.push(" interface BazisPermissionSources {", ...[...permissionSourcePrefixes].sort().map((prefix) => ` ${JSON.stringify(prefix)}: true;`), " }");
729
+ }
730
+ lines.push("}", "");
721
731
  return lines.join("\n");
722
732
  }
723
733
 
@@ -1036,6 +1046,9 @@ function collectDiClassRegistrations(source: ts.SourceFile): void {
1036
1046
  // `references: { "orders.order": OrderReferences }`: the field registers the classes.
1037
1047
  const references = objectFor(properties.get("references"));
1038
1048
  if (references) for (const entry of propertiesOf(references).values()) record(entry);
1049
+ // `permissionSources: { data: DataPermissionSource }`: the field registers the classes too.
1050
+ const permissionSources = objectFor(properties.get("permissionSources"));
1051
+ if (permissionSources) for (const entry of propertiesOf(permissionSources).values()) record(entry);
1039
1052
  // `permissions: orderPermissions`: the field registers the `lookup` classes its permissions name.
1040
1053
  for (const lookup of permissionLookups(properties.get("permissions"))) record(lookup);
1041
1054
  const imports = properties.get("imports");
@@ -1177,6 +1190,22 @@ function collectProviderFormErrors(source: ts.SourceFile): void {
1177
1190
  }
1178
1191
  permissionDeclarations.set(`${file}\0${exported.name}`, { name: exported.name, file });
1179
1192
  };
1193
+ /**
1194
+ * `permissionSources: { data: DataPermissionSource }`: the prefixes go to the
1195
+ * generated `permissions.ts`, which makes every key under them a permission
1196
+ * key for `can` and `access.filter`. A framework pack names its own in its sources.
1197
+ */
1198
+ const collectPermissionSources = (module: string, initializer: ts.Expression): void => {
1199
+ const file = normalizeProjectPath(initializer.getSourceFile().fileName);
1200
+ if (file.includes("/node_modules/") || isFrameworkPackPath(file)) return;
1201
+ const object = resolved(initializer, ts.isObjectLiteralExpression);
1202
+ const named = object?.properties.every((property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)));
1203
+ if (!object || named !== true) {
1204
+ fatalErrors.push(`BAZIS_ACCESS_SOURCES_FORM: ${sourceLocation(initializer)}: permissionSources of ${module}: "${text(initializer)}" — write an object "prefix: Class" in the module, so that the keys under the prefixes become the type of can(...).`);
1205
+ return;
1206
+ }
1207
+ for (const property of object.properties) permissionSourcePrefixes.add((property.name as ts.Identifier | ts.StringLiteralLike).text);
1208
+ };
1180
1209
  const checkModule = (module: string, argument: ts.Expression | undefined): void => {
1181
1210
  const object = argument && resolved(argument, ts.isObjectLiteralExpression);
1182
1211
  if (!object) return;
@@ -1184,6 +1213,7 @@ function collectProviderFormErrors(source: ts.SourceFile): void {
1184
1213
  if (!ts.isPropertyAssignment(property)) continue;
1185
1214
  const field = property.name.getText().replace(/["']/g, "");
1186
1215
  if (field === "permissions") collectPermissions(module, property.initializer);
1216
+ if (field === "permissionSources") collectPermissionSources(module, property.initializer);
1187
1217
  if (field !== "providers" && !CLASS_LISTS.includes(field)) continue;
1188
1218
  const list = resolved(property.initializer, ts.isArrayLiteralExpression);
1189
1219
  if (!list) {
@@ -22,7 +22,7 @@ export class ChatGateway {
22
22
  }
23
23
  ```
24
24
 
25
- Declare it in its module, like a controller; `runApp` serves it on the HTTP listener:
25
+ Declare it in its module, like a controller; the application started by `runApp(Application)` serves it on the HTTP listener:
26
26
 
27
27
  ```ts
28
28
  import { Module, singleton } from "@/core/di";
@@ -34,7 +34,7 @@ export class ChatModule {}
34
34
  export class Application {}
35
35
  ```
36
36
 
37
- Without `runApp`, `websocketModule({ imports: [ChatModule], ... })` next to
37
+ Without `@App`, `websocketModule({ imports: [ChatModule], ... })` next to
38
38
  `httpModule` does the same. Services send with `WebSocketService`:
39
39
  `this.ws.to("user:ann").emit("order-paid", { id })`.
40
40
 
package/index.ts CHANGED
@@ -84,6 +84,8 @@ export {
84
84
  AgentRuntimeError,
85
85
  AgentSemanticError,
86
86
  AgentModelTimeoutError,
87
+ AgentModelUnavailableError,
88
+ AgentModelContextOverflowError,
87
89
  AgentToolExecutionError,
88
90
  ToolError,
89
91
  ApproximateAgentContextTokenEstimator,
@@ -94,6 +96,7 @@ export {
94
96
  agentInvocation,
95
97
  agentJsonSchema,
96
98
  agentMessage,
99
+ agentMessageText,
97
100
  agentModelCapabilities,
98
101
  agentModelRequest,
99
102
  agentModelResponse,
@@ -117,6 +120,8 @@ export {
117
120
  type AgentOutputContract,
118
121
  type AgentRuntimeInvokeOptions,
119
122
  type AgentRuntimeOptions,
123
+ type AgentModelRetryPolicy,
124
+ type AgentCompactionOptions,
120
125
  type AgentRuntimeResult,
121
126
  type AgentRuntimeStatus,
122
127
  type AgentRuntimeToolExecutionOptions,
@@ -284,14 +284,16 @@ class MyModule {}
284
284
 
285
285
  ### Explicit dependencies in a provider
286
286
 
287
+ An application never lists dependencies: codegen reads the constructor
288
+ (`scoped(UserService)` is enough). A package that extends the framework and builds
289
+ providers by hand takes the low-level forms from `bazis/core/di/lowLevel` (0.138.0):
290
+
287
291
  ```ts
288
- import { scoped, DI } from "@/core/di";
289
- import { repositoryFor } from "@/core/orm";
292
+ import { DI } from "bazis/core/di/lowLevel";
293
+ import { repositoryFor } from "bazis/core/orm";
290
294
 
291
295
  providers: [
292
- scoped(
293
- DI.classProvider(UserService, UserService, [repositoryFor(User)] as const),
294
- ),
296
+ DI.scoped(DI.classProvider(UserService, UserService, [repositoryFor(User)] as const)),
295
297
  ],
296
298
  ```
297
299
 
@@ -385,12 +387,11 @@ export class UserService {
385
387
 
386
388
  ```ts
387
389
  import { Module, scoped } from "@/core/di";
388
- import { repositoryFor } from "@/core/orm";
389
390
 
390
391
  @Module({
391
392
  imports: [DataModule],
392
393
  providers: [
393
- scoped(UserService, UserService, [repositoryFor(User)] as const),
394
+ scoped(UserService), // codegen reads `IRepository<User>` from the constructor
394
395
  ],
395
396
  exports: [UserService],
396
397
  })
@@ -517,7 +518,9 @@ books.add(book);
517
518
  `di:generate` wires a constructor parameter typed `IRepository<User>` automatically. Explicit deps are needed only when the class is registered without codegen (for example in a hand-built test container):
518
519
 
519
520
  ```ts
520
- scoped(UserService, UserService, [repositoryFor(User)] as const)
521
+ import { DI } from "bazis/core/di/lowLevel"; // since 0.138.0; not in bazis/core/di
522
+
523
+ DI.scoped(DI.classProvider(UserService, UserService, [repositoryFor(User)] as const))
521
524
  ```
522
525
 
523
526
  ### 4. `ensureCreated` in tests without starting the application
@@ -596,7 +599,7 @@ A mock usually needs only the methods the service calls.
596
599
  For **controller** tests it is enough to replace the store contract the controller depends on (here a hypothetical `IUserStore`):
597
600
 
598
601
  ```ts
599
- import { Module, createContainer, DI } from "bazis/core/di";
602
+ import { Module, createContainer, singletonValue } from "bazis/core/di";
600
603
  import { InMemoryUserStore } from "./InMemoryUserStore";
601
604
  import { IUserStore } from "./IUserStore";
602
605
  import { UsersController } from "./UsersController";
@@ -604,7 +607,7 @@ import { UsersController } from "./UsersController";
604
607
  @Module({
605
608
  controllers: [UsersController],
606
609
  providers: [
607
- DI.scoped(DI.valueProvider(IUserStore, new InMemoryUserStore())),
610
+ singletonValue(IUserStore, new InMemoryUserStore()),
608
611
  ],
609
612
  })
610
613
  class TestUsersModule {}
@@ -652,7 +655,7 @@ expect(await service.list(10)).toHaveLength(1);
652
655
  1. **Do not mock `query()`** if you test SQL/filters: use PostgreSQL qualification.
653
656
  2. **Mock `saveChanges()`** to check that the service saves data at all.
654
657
  3. **One scope per test**: `scope.dispose()` in `afterEach`, otherwise scoped services leak.
655
- 4. In hand-built DI test containers (without codegen) with `validateOnBuild: true`, give Repository deps explicitly: `[repositoryFor(User)]`.
658
+ 4. In hand-built DI test containers (without codegen) with `validateOnBuild: true`, give Repository deps explicitly through `DI.classProvider(…, [repositoryFor(User)])` from `bazis/core/di/lowLevel`. With codegen, prefer `createTestContainer(Application, { overrides })` from `bazis/core/testing`.
656
659
 
657
660
  ---
658
661
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.138.0",
3
+ "version": "0.140.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",
@@ -42,6 +42,7 @@
42
42
  "./core/http-client": "./core/http-client/index.ts",
43
43
  "./core/http/OpenApi/generatedOpenApiRegistry": "./core/http/OpenApi/generatedOpenApiRegistry.ts",
44
44
  "./core/infra": "./core/infra/index.ts",
45
+ "./core/infra/anthropic": "./core/infra/anthropic.ts",
45
46
  "./core/kernel": "./core/kernel/index.ts",
46
47
  "./core/orm": "./core/orm/index.ts",
47
48
  "./core/testing": "./core/testing/index.ts",
@@ -81,7 +82,8 @@
81
82
  },
82
83
  "peerDependencies": {
83
84
  "typescript": "^5.9.3 || ^6.0.0 || ^7.0.0",
84
- "@typescript/typescript6": "^6.0.0"
85
+ "@typescript/typescript6": "^6.0.0",
86
+ "@anthropic-ai/sdk": "^1.8.0"
85
87
  },
86
88
  "publishConfig": {
87
89
  "access": "public",
@@ -90,6 +92,9 @@
90
92
  "peerDependenciesMeta": {
91
93
  "@typescript/typescript6": {
92
94
  "optional": true
95
+ },
96
+ "@anthropic-ai/sdk": {
97
+ "optional": true
93
98
  }
94
99
  }
95
100
  }