agentlang 0.10.8 → 0.11.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 (121) hide show
  1. package/README.md +60 -0
  2. package/out/api/http.d.ts.map +1 -1
  3. package/out/api/http.js +64 -39
  4. package/out/api/http.js.map +1 -1
  5. package/out/cli/main.d.ts +1 -1
  6. package/out/cli/main.d.ts.map +1 -1
  7. package/out/cli/main.js +7 -3
  8. package/out/cli/main.js.map +1 -1
  9. package/out/extension/main.cjs +250 -250
  10. package/out/extension/main.cjs.map +2 -2
  11. package/out/language/main.cjs +504 -504
  12. package/out/language/main.cjs.map +3 -3
  13. package/out/runtime/auth/cognito.d.ts.map +1 -1
  14. package/out/runtime/auth/cognito.js +129 -64
  15. package/out/runtime/auth/cognito.js.map +1 -1
  16. package/out/runtime/defs.d.ts +22 -9
  17. package/out/runtime/defs.d.ts.map +1 -1
  18. package/out/runtime/defs.js +44 -9
  19. package/out/runtime/defs.js.map +1 -1
  20. package/out/runtime/document-retriever.d.ts +24 -0
  21. package/out/runtime/document-retriever.d.ts.map +1 -0
  22. package/out/runtime/document-retriever.js +258 -0
  23. package/out/runtime/document-retriever.js.map +1 -0
  24. package/out/runtime/errors/coded-error.d.ts +8 -0
  25. package/out/runtime/errors/coded-error.d.ts.map +1 -0
  26. package/out/runtime/errors/coded-error.js +13 -0
  27. package/out/runtime/errors/coded-error.js.map +1 -0
  28. package/out/runtime/errors/http-error.d.ts +25 -0
  29. package/out/runtime/errors/http-error.d.ts.map +1 -0
  30. package/out/runtime/errors/http-error.js +169 -0
  31. package/out/runtime/errors/http-error.js.map +1 -0
  32. package/out/runtime/excel-resolver.d.ts +4 -0
  33. package/out/runtime/excel-resolver.d.ts.map +1 -0
  34. package/out/runtime/excel-resolver.js +96 -0
  35. package/out/runtime/excel-resolver.js.map +1 -0
  36. package/out/runtime/excel.d.ts +25 -0
  37. package/out/runtime/excel.d.ts.map +1 -0
  38. package/out/runtime/excel.js +127 -0
  39. package/out/runtime/excel.js.map +1 -0
  40. package/out/runtime/exec-graph-cache.d.ts +5 -0
  41. package/out/runtime/exec-graph-cache.d.ts.map +1 -0
  42. package/out/runtime/exec-graph-cache.js +9 -0
  43. package/out/runtime/exec-graph-cache.js.map +1 -0
  44. package/out/runtime/exec-graph.d.ts.map +1 -1
  45. package/out/runtime/exec-graph.js +2 -1
  46. package/out/runtime/exec-graph.js.map +1 -1
  47. package/out/runtime/interpreter.d.ts.map +1 -1
  48. package/out/runtime/interpreter.js +30 -27
  49. package/out/runtime/interpreter.js.map +1 -1
  50. package/out/runtime/loader.d.ts.map +1 -1
  51. package/out/runtime/loader.js +5 -1
  52. package/out/runtime/loader.js.map +1 -1
  53. package/out/runtime/logger.d.ts +6 -0
  54. package/out/runtime/logger.d.ts.map +1 -1
  55. package/out/runtime/logger.js +21 -0
  56. package/out/runtime/logger.js.map +1 -1
  57. package/out/runtime/module.d.ts.map +1 -1
  58. package/out/runtime/module.js +14 -13
  59. package/out/runtime/module.js.map +1 -1
  60. package/out/runtime/modules/ai.d.ts +2 -1
  61. package/out/runtime/modules/ai.d.ts.map +1 -1
  62. package/out/runtime/modules/ai.js +7 -2
  63. package/out/runtime/modules/ai.js.map +1 -1
  64. package/out/runtime/modules/auth.d.ts.map +1 -1
  65. package/out/runtime/modules/auth.js +44 -16
  66. package/out/runtime/modules/auth.js.map +1 -1
  67. package/out/runtime/modules/core.d.ts +6 -0
  68. package/out/runtime/modules/core.d.ts.map +1 -1
  69. package/out/runtime/modules/core.js +20 -0
  70. package/out/runtime/modules/core.js.map +1 -1
  71. package/out/runtime/resolvers/sqldb/database.d.ts.map +1 -1
  72. package/out/runtime/resolvers/sqldb/database.js +76 -39
  73. package/out/runtime/resolvers/sqldb/database.js.map +1 -1
  74. package/out/runtime/resolvers/sqldb/db-errors.d.ts +6 -0
  75. package/out/runtime/resolvers/sqldb/db-errors.d.ts.map +1 -0
  76. package/out/runtime/resolvers/sqldb/db-errors.js +100 -0
  77. package/out/runtime/resolvers/sqldb/db-errors.js.map +1 -0
  78. package/out/runtime/resolvers/vector/lancedb-store.d.ts +16 -0
  79. package/out/runtime/resolvers/vector/lancedb-store.d.ts.map +1 -0
  80. package/out/runtime/resolvers/vector/lancedb-store.js +159 -0
  81. package/out/runtime/resolvers/vector/lancedb-store.js.map +1 -0
  82. package/out/runtime/resolvers/vector/types.d.ts +32 -0
  83. package/out/runtime/resolvers/vector/types.d.ts.map +1 -0
  84. package/out/runtime/resolvers/vector/types.js +2 -0
  85. package/out/runtime/resolvers/vector/types.js.map +1 -0
  86. package/out/runtime/state.d.ts +3 -0
  87. package/out/runtime/state.d.ts.map +1 -1
  88. package/out/runtime/state.js +7 -0
  89. package/out/runtime/state.js.map +1 -1
  90. package/out/setupClassic.d.ts +98 -0
  91. package/out/setupClassic.d.ts.map +1 -0
  92. package/out/setupClassic.js +38 -0
  93. package/out/setupClassic.js.map +1 -0
  94. package/out/setupCommon.d.ts +2 -0
  95. package/out/setupCommon.d.ts.map +1 -0
  96. package/out/setupCommon.js +33 -0
  97. package/out/setupCommon.js.map +1 -0
  98. package/out/setupExtended.d.ts +40 -0
  99. package/out/setupExtended.d.ts.map +1 -0
  100. package/out/setupExtended.js +67 -0
  101. package/out/setupExtended.js.map +1 -0
  102. package/package.json +19 -18
  103. package/src/api/http.ts +71 -37
  104. package/src/cli/main.ts +12 -4
  105. package/src/runtime/auth/cognito.ts +187 -65
  106. package/src/runtime/defs.ts +51 -18
  107. package/src/runtime/errors/coded-error.ts +18 -0
  108. package/src/runtime/errors/http-error.ts +197 -0
  109. package/src/runtime/exec-graph-cache.ts +12 -0
  110. package/src/runtime/exec-graph.ts +2 -2
  111. package/src/runtime/interpreter.ts +73 -28
  112. package/src/runtime/loader.ts +5 -0
  113. package/src/runtime/logger.ts +27 -0
  114. package/src/runtime/module.ts +45 -13
  115. package/src/runtime/modules/ai.ts +11 -2
  116. package/src/runtime/modules/auth.ts +45 -18
  117. package/src/runtime/modules/core.ts +26 -0
  118. package/src/runtime/resolvers/sqldb/database.ts +88 -37
  119. package/src/runtime/resolvers/sqldb/db-errors.ts +113 -0
  120. package/src/runtime/state.ts +7 -0
  121. package/src/xlsx.d.ts +17 -0
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { createCodedError } from './errors/coded-error.js';
2
3
  import {
3
4
  AttributeDefinition,
4
5
  Expr,
@@ -2631,7 +2632,11 @@ export class Module {
2631
2632
 
2632
2633
  getEntry(entryName: string): ModuleEntry {
2633
2634
  const idx: number = this.getEntryIndex(entryName);
2634
- if (idx < 0) throw new Error(`Entry ${entryName} not found in module ${this.name}`);
2635
+ if (idx < 0)
2636
+ throw createCodedError(
2637
+ `Entry ${entryName} not found in module ${this.name}`,
2638
+ 'AL_MOD_ENTRY_NOT_FOUND'
2639
+ );
2635
2640
  return this.entries[idx];
2636
2641
  }
2637
2642
 
@@ -2646,7 +2651,10 @@ export class Module {
2646
2651
  if (e instanceof Record) {
2647
2652
  return e as Record;
2648
2653
  }
2649
- throw new Error(`${recordName} is not a record in module ${this.name}`);
2654
+ throw createCodedError(
2655
+ `${recordName} is not a record in module ${this.name}`,
2656
+ 'AL_MOD_NOT_A_RECORD'
2657
+ );
2650
2658
  }
2651
2659
 
2652
2660
  removeEntry(entryName: string): boolean {
@@ -2942,7 +2950,7 @@ export function isModule(name: string): boolean {
2942
2950
  export function fetchModule(moduleName: string): Module {
2943
2951
  const module: Module | undefined = getModuleDb().get(moduleName);
2944
2952
  if (module === undefined) {
2945
- throw new Error(`Module not found - ${moduleName}`);
2953
+ throw createCodedError(`Module not found - ${moduleName}`, 'AL_MOD_MODULE_NOT_FOUND');
2946
2954
  }
2947
2955
  return module;
2948
2956
  }
@@ -3481,7 +3489,7 @@ export function getEntity(name: string, moduleName: string): Entity | undefined
3481
3489
  export function fetchEntity(path: Path): Entity {
3482
3490
  const e = getEntity(path.getEntryName(), path.getModuleName());
3483
3491
  if (e === undefined) {
3484
- throw new Error(`Entity not found - ${path.asFqName()}`);
3492
+ throw createCodedError(`Entity not found - ${path.asFqName()}`, 'AL_MOD_ENTITY_NOT_FOUND');
3485
3493
  }
3486
3494
  return e;
3487
3495
  }
@@ -3513,7 +3521,10 @@ export function getEvent(name: string, moduleName: string): Event {
3513
3521
  if (fr.module.isEvent(fr.entryName)) {
3514
3522
  return fr.module.getEntry(fr.entryName) as Event;
3515
3523
  }
3516
- throw new Error(`Event ${fr.entryName} not found in module ${fr.moduleName}`);
3524
+ throw createCodedError(
3525
+ `Event ${fr.entryName} not found in module ${fr.moduleName}`,
3526
+ 'AL_MOD_EVENT_NOT_FOUND'
3527
+ );
3517
3528
  }
3518
3529
 
3519
3530
  export function maybeGetEvent(name: string, moduleName: string): Event | undefined {
@@ -3529,7 +3540,10 @@ export function getRecord(name: string, moduleName: string): Record {
3529
3540
  if (fr.module.isRecord(fr.entryName)) {
3530
3541
  return fr.module.getEntry(fr.entryName) as Record;
3531
3542
  }
3532
- throw new Error(`Record ${fr.entryName} not found in module ${fr.moduleName}`);
3543
+ throw createCodedError(
3544
+ `Record ${fr.entryName} not found in module ${fr.moduleName}`,
3545
+ 'AL_MOD_RECORD_NOT_FOUND'
3546
+ );
3533
3547
  }
3534
3548
 
3535
3549
  export function getRelationship(name: string, moduleName: string): Relationship {
@@ -3537,7 +3551,10 @@ export function getRelationship(name: string, moduleName: string): Relationship
3537
3551
  if (fr.module.isRelationship(fr.entryName)) {
3538
3552
  return fr.module.getEntry(fr.entryName) as Relationship;
3539
3553
  }
3540
- throw new Error(`Relationship ${fr.entryName} not found in module ${fr.moduleName}`);
3554
+ throw createCodedError(
3555
+ `Relationship ${fr.entryName} not found in module ${fr.moduleName}`,
3556
+ 'AL_MOD_RELATIONSHIP_NOT_FOUND'
3557
+ );
3541
3558
  }
3542
3559
 
3543
3560
  export function getAllBetweenRelationships(): Relationship[] {
@@ -3707,7 +3724,10 @@ function checkOneOfValue(attrSpec: AttributeSpec, attrName: string, attrValue: a
3707
3724
  const vals: Set<string> | undefined = getEnumValues(attrSpec);
3708
3725
  if (vals) {
3709
3726
  if (!vals.has(attrValue as string)) {
3710
- throw new Error(`Value of ${attrName} must be one of [${[...vals]}]`);
3727
+ throw createCodedError(
3728
+ `Value of ${attrName} must be one of [${[...vals]}]`,
3729
+ 'AL_MOD_TYPE_MISMATCH'
3730
+ );
3711
3731
  }
3712
3732
  return true;
3713
3733
  }
@@ -3725,7 +3745,10 @@ function getCheckPredicate(attrSpec: AttributeSpec): any {
3725
3745
  function validateType(attrName: string, attrValue: any, attrSpec: AttributeSpec) {
3726
3746
  if (attrSpec.type == 'Path') {
3727
3747
  if (!isPath(attrValue, getRefSpec(attrSpec))) {
3728
- throw new Error(`Failed to validate Path ${attrValue} passed to ${attrName}`);
3748
+ throw createCodedError(
3749
+ `Failed to validate Path ${attrValue} passed to ${attrName}`,
3750
+ 'AL_MOD_TYPE_MISMATCH'
3751
+ );
3729
3752
  }
3730
3753
  }
3731
3754
  let predic = getCheckPredicate(attrSpec);
@@ -3733,16 +3756,22 @@ function validateType(attrName: string, attrValue: any, attrSpec: AttributeSpec)
3733
3756
  if (predic !== undefined) {
3734
3757
  if (isArrayAttribute(attrSpec)) {
3735
3758
  if (!(attrValue instanceof Array)) {
3736
- throw new Error(`${attrName} expects an array of values`);
3759
+ throw createCodedError(`${attrName} expects an array of values`, 'AL_MOD_TYPE_MISMATCH');
3737
3760
  } else {
3738
3761
  if (!attrValue.every(predic)) {
3739
- throw new Error(`Invalid value in the array passed to ${attrName}`);
3762
+ throw createCodedError(
3763
+ `Invalid value in the array passed to ${attrName}`,
3764
+ 'AL_MOD_TYPE_MISMATCH'
3765
+ );
3740
3766
  }
3741
3767
  }
3742
3768
  } else {
3743
3769
  if (!checkOneOfValue(attrSpec, attrName, attrValue)) {
3744
3770
  if (!predic(attrValue)) {
3745
- throw new Error(`Invalid value ${attrValue} specified for ${attrName}`);
3771
+ throw createCodedError(
3772
+ `Invalid value ${attrValue} specified for ${attrName}`,
3773
+ 'AL_MOD_TYPE_MISMATCH'
3774
+ );
3746
3775
  }
3747
3776
  }
3748
3777
  }
@@ -4307,7 +4336,10 @@ export function makeInstance(
4307
4336
  if (schema.size > 0) {
4308
4337
  attributes.forEach((value: any, key: string) => {
4309
4338
  if (!schema.has(key)) {
4310
- throw new Error(`Invalid attribute '${key}' specified for ${moduleName}/${entryName}`);
4339
+ throw createCodedError(
4340
+ `Invalid attribute '${key}' specified for ${moduleName}/${entryName}`,
4341
+ 'AL_MOD_INVALID_ATTR'
4342
+ );
4311
4343
  }
4312
4344
  const spec: AttributeSpec = getAttributeSpec(schema, key);
4313
4345
  if (value !== null && value !== undefined) validateType(key, value, spec);
@@ -85,6 +85,7 @@ const AgentEvalType = 'eval';
85
85
  // --- Agent cancellation infrastructure ---
86
86
 
87
87
  export class AgentCancelledException extends Error {
88
+ readonly agentlangCode = 'AL_AGENT_CANCELLED';
88
89
  constructor(chatId: string) {
89
90
  super(`Agent cancelled for chatId: ${chatId}`);
90
91
  this.name = 'AgentCancelledException';
@@ -1286,8 +1287,16 @@ async function parseHelper(stmt: string, env: Environment): Promise<any> {
1286
1287
  return env.getLastResult();
1287
1288
  }
1288
1289
 
1289
- export async function findAgentByName(name: string, env: Environment): Promise<AgentInstance> {
1290
- const result = await parseHelper(`{${AgentFqName} {name? "${name}"}}`, env);
1290
+ export async function findAgentByName(name: string, _env: Environment): Promise<AgentInstance> {
1291
+ // Match findProviderForLLM: Agent rows are upserted at load under GlobalEnvironment / bootstrap
1292
+ // tenant. A request env (e.g. after setActiveEvent) may not see those rows; use a GlobalEnvironment
1293
+ // child for the lookup.
1294
+ const lookupEnv = new Environment('agent-by-name-lookup', GlobalEnvironment);
1295
+ const result = await parseAndEvaluateStatement(
1296
+ `{${AgentFqName} {name? "${name}"}}`,
1297
+ undefined,
1298
+ lookupEnv
1299
+ );
1291
1300
  if (result instanceof Array && result.length > 0) {
1292
1301
  const agentInstance: Instance = result[0];
1293
1302
  return AgentInstance.FromInstance(agentInstance);
@@ -27,6 +27,7 @@ import {
27
27
  set_getUserTenantId,
28
28
  PathAttributeName,
29
29
  } from '../defs.js';
30
+ import { createCodedError } from '../errors/coded-error.js';
30
31
  import {
31
32
  DbContext,
32
33
  getManyByRawQuery,
@@ -934,6 +935,11 @@ async function updatePermissionCacheForRole(role: string, env: Environment) {
934
935
  }
935
936
  }
936
937
 
938
+ /** Matches the built-in superuser role name (case-insensitive). */
939
+ function isGlobalAdminRoleName(role: string | undefined | null): boolean {
940
+ return typeof role === 'string' && role.length > 0 && role.toLowerCase() === 'admin';
941
+ }
942
+
937
943
  export async function userHasPermissions(
938
944
  userId: string,
939
945
  resourceFqName: string,
@@ -959,6 +965,10 @@ export async function userHasPermissions(
959
965
  }
960
966
  UserRoleCache.set(userId, userRoles);
961
967
  }
968
+ // Real admin users keep full access even when agents/workflows set assumedRole (narrowing).
969
+ if (userRoles?.some((r: string) => isGlobalAdminRoleName(r))) {
970
+ return true;
971
+ }
962
972
  let tempRoles = userRoles;
963
973
  const assumedRole = env.getAssumedRole();
964
974
  if (assumedRole) {
@@ -967,12 +977,7 @@ export async function userHasPermissions(
967
977
  await updatePermissionCacheForRole(assumedRole, env);
968
978
  }
969
979
  }
970
- if (
971
- tempRoles &&
972
- tempRoles.find((role: string) => {
973
- return role === 'admin';
974
- })
975
- ) {
980
+ if (tempRoles?.some((r: string) => isGlobalAdminRoleName(r))) {
976
981
  return true;
977
982
  }
978
983
  const [c, r, u, d] = [
@@ -1041,7 +1046,7 @@ function fetchAuthImpl(): AgentlangAuth {
1041
1046
  if (runtimeAuth) {
1042
1047
  return runtimeAuth;
1043
1048
  } else {
1044
- throw new Error('Auth not initialized');
1049
+ throw createCodedError('Auth not initialized', 'AL_AUTH_NOT_INITIALIZED');
1045
1050
  }
1046
1051
  }
1047
1052
 
@@ -1111,7 +1116,9 @@ export async function forgotPasswordUser(username: string, env: Environment): Pr
1111
1116
  const email = username.toLowerCase();
1112
1117
  const allowed = await fetchAuthImpl().userExistsInIdentityProvider(email, env);
1113
1118
  if (!allowed) {
1114
- throw new UserNotFoundError('Email not registered');
1119
+ throw new UserNotFoundError('Email not registered', {
1120
+ agentlangCode: 'AL_AUTH_EMAIL_NOT_REGISTERED',
1121
+ });
1115
1122
  }
1116
1123
  await fetchAuthImpl().forgotPassword(email, env);
1117
1124
  return { status: 'ok', message: 'Password reset code sent' };
@@ -1261,7 +1268,9 @@ export async function changePassword(
1261
1268
  return undefined;
1262
1269
  }
1263
1270
  } else {
1264
- throw new UnauthorisedError(`No active session for user ${user}`);
1271
+ throw new UnauthorisedError(`No active session for user ${user}`, {
1272
+ agentlangCode: 'AL_AUTH_NO_ACTIVE_SESSION_CHANGE_PW',
1273
+ });
1265
1274
  }
1266
1275
  }
1267
1276
 
@@ -1288,7 +1297,9 @@ async function verifyJwtToken(token: string, env?: Environment): Promise<ActiveS
1288
1297
  try {
1289
1298
  // Validate JWT structure first
1290
1299
  if (!isJwtToken(token)) {
1291
- throw new UnauthorisedError('Invalid JWT token structure');
1300
+ throw new UnauthorisedError('Invalid JWT token structure', {
1301
+ agentlangCode: 'AL_AUTH_JWT_STRUCTURE_INVALID',
1302
+ });
1292
1303
  }
1293
1304
 
1294
1305
  // Verify the JWT token directly with Cognito
@@ -1303,7 +1314,9 @@ async function verifyJwtToken(token: string, env?: Environment): Promise<ActiveS
1303
1314
  const email = payload.email || payload['cognito:username'];
1304
1315
 
1305
1316
  if (!userId) {
1306
- throw new UnauthorisedError('Invalid JWT token: missing user identifier');
1317
+ throw new UnauthorisedError('Invalid JWT token: missing user identifier', {
1318
+ agentlangCode: 'AL_AUTH_JWT_MISSING_USER_ID',
1319
+ });
1307
1320
  }
1308
1321
 
1309
1322
  let localUser = null;
@@ -1319,7 +1332,9 @@ async function verifyJwtToken(token: string, env?: Environment): Promise<ActiveS
1319
1332
  logger.warn(
1320
1333
  `User not found in local database for JWT token. Email: ${email}, UserId: ${userId}`
1321
1334
  );
1322
- throw new UnauthorisedError(`User not found in local database`);
1335
+ throw new UnauthorisedError(`User not found in local database`, {
1336
+ agentlangCode: 'AL_AUTH_JWT_USER_NOT_IN_DB',
1337
+ });
1323
1338
  }
1324
1339
 
1325
1340
  // Use the local user's ID for consistency
@@ -1328,12 +1343,16 @@ async function verifyJwtToken(token: string, env?: Environment): Promise<ActiveS
1328
1343
  // Check if user status is 'Active'
1329
1344
  const userStatus = localUser.lookup('status');
1330
1345
  if (userStatus !== 'Active') {
1331
- throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`);
1346
+ throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`, {
1347
+ agentlangCode: 'AL_AUTH_USER_NOT_ACTIVE_JWT',
1348
+ });
1332
1349
  }
1333
1350
 
1334
1351
  const sess = await findUserSession(localUserId, env);
1335
1352
  if (!sess) {
1336
- throw new UnauthorisedError(`No session found for user ${email}, UserId: ${userId}`);
1353
+ throw new UnauthorisedError(`No session found for user ${email}, UserId: ${userId}`, {
1354
+ agentlangCode: 'AL_AUTH_NO_SESSION_JWT',
1355
+ });
1337
1356
  }
1338
1357
  // For JWT tokens, we use the token itself as sessionId for tracking
1339
1358
  return { sessionId: sess.lookup('id'), userId: localUserId };
@@ -1345,7 +1364,9 @@ async function verifyJwtToken(token: string, env?: Environment): Promise<ActiveS
1345
1364
  errorName: err.name,
1346
1365
  errorMessage: err.message,
1347
1366
  });
1348
- throw new UnauthorisedError('JWT token verification failed');
1367
+ throw new UnauthorisedError('JWT token verification failed', {
1368
+ agentlangCode: 'AL_AUTH_JWT_VERIFY_WRAPPER',
1369
+ });
1349
1370
  }
1350
1371
  };
1351
1372
  if (needCommit) {
@@ -1368,7 +1389,9 @@ async function verifySessionToken(token: string, env?: Environment): Promise<Act
1368
1389
  if (user) {
1369
1390
  const userStatus = user.lookup('status');
1370
1391
  if (userStatus !== 'Active') {
1371
- throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`);
1392
+ throw new UnauthorisedError(`User account is not active. Status: ${userStatus}`, {
1393
+ agentlangCode: 'AL_AUTH_USER_NOT_ACTIVE_SESSION',
1394
+ });
1372
1395
  }
1373
1396
  }
1374
1397
 
@@ -1378,7 +1401,9 @@ async function verifySessionToken(token: string, env?: Environment): Promise<Act
1378
1401
  return { sessionId: sessId, userId: userId };
1379
1402
  } else {
1380
1403
  logger.warn(`No active session found for user '${userId}'`);
1381
- throw new UnauthorisedError(`No active session for user '${userId}'`);
1404
+ throw new UnauthorisedError(`No active session for user '${userId}'`, {
1405
+ agentlangCode: 'AL_AUTH_NO_ACTIVE_SESSION_TOKEN',
1406
+ });
1382
1407
  }
1383
1408
  } catch (err: any) {
1384
1409
  if (err instanceof UnauthorisedError) {
@@ -1390,7 +1415,9 @@ async function verifySessionToken(token: string, env?: Environment): Promise<Act
1390
1415
  errorMessage: err.message,
1391
1416
  sessionId: sessId,
1392
1417
  });
1393
- throw new UnauthorisedError('Session verification failed');
1418
+ throw new UnauthorisedError('Session verification failed', {
1419
+ agentlangCode: 'AL_AUTH_SESSION_VERIFY_WRAPPER',
1420
+ });
1394
1421
  }
1395
1422
  };
1396
1423
  if (needCommit) {
@@ -167,6 +167,16 @@ entity Migration {
167
167
  downs String @optional
168
168
  }
169
169
 
170
+ // Custom HTTP error messages. \`target\` is "*" for a universal (code-only) override,
171
+ // or "module/Entry" for an entity-specific one. \`message\` may contain the
172
+ // {{code}} and {{message}} placeholders, substituted at resolution time.
173
+ entity errorMessage {
174
+ id UUID @id @default(uuid()),
175
+ target String @default("*") @indexed,
176
+ code String @indexed,
177
+ message String
178
+ }
179
+
170
180
  @public event Query {
171
181
  q Any
172
182
  }
@@ -283,6 +293,22 @@ export async function lookupTimersWithRunningStatus(): Promise<Instance[]> {
283
293
  return await parseAndEvaluateStatement(`{agentlang/timer {status? "R"}}`);
284
294
  }
285
295
 
296
+ /**
297
+ * Look up a custom error-message template stored in the `agentlang/errorMessage`
298
+ * system entity. `target` is "*" for a universal override or "module/Entry" for an
299
+ * entity-specific one. Returns the stored `message`, or undefined when none exists.
300
+ */
301
+ export async function lookupCustomErrorMessage(
302
+ target: string,
303
+ code: string
304
+ ): Promise<string | undefined> {
305
+ const r: any = await parseAndEvaluateStatement(
306
+ `{${DefaultModuleName}/errorMessage {target? "${escapeSpecialChars(target)}", code? "${escapeSpecialChars(code)}"}}`
307
+ );
308
+ const insts: Instance[] = Array.isArray(r) ? r : r ? [r] : [];
309
+ return insts.length > 0 ? insts[0].lookup('message') : undefined;
310
+ }
311
+
286
312
  async function addAudit(
287
313
  env: Environment,
288
314
  action: 'c' | 'd' | 'u',
@@ -45,6 +45,8 @@ import {
45
45
  TenantAttributeName,
46
46
  UnauthorisedError,
47
47
  } from '../../defs.js';
48
+ import { createCodedError } from '../../errors/coded-error.js';
49
+ import { mapDatabaseError } from './db-errors.js';
48
50
  import { saveMigration } from '../../modules/core.js';
49
51
  import { getAppSpec } from '../../loader.js';
50
52
  import { WhereClause } from '../interface.js';
@@ -393,9 +395,10 @@ async function validateSchemaInProd(dataSource: DataSource) {
393
395
  );
394
396
  if (pendingQueries.length > 0) {
395
397
  const pending = pendingQueries.join('\n ');
396
- throw new Error(
398
+ throw createCodedError(
397
399
  `Schema mismatch detected: the app model does not match the database schema. ` +
398
- `Run migrations before starting in production mode.\n Pending changes:\n ${pending}`
400
+ `Run migrations before starting in production mode.\n Pending changes:\n ${pending}`,
401
+ 'AL_DB_SCHEMA_MISMATCH_PROD'
399
402
  );
400
403
  }
401
404
  }
@@ -431,8 +434,9 @@ async function maybeHandleMigrations(dataSource: DataSource) {
431
434
  const simulation = await simulateMigration(dataSource);
432
435
  if (!simulation.success) {
433
436
  logger.error(`Migration simulation failed:\n ${simulation.errors.join('\n ')}`);
434
- throw new Error(
435
- `Migration aborted: simulation failed.\n ${simulation.errors.join('\n ')}`
437
+ throw createCodedError(
438
+ `Migration aborted: simulation failed.\n ${simulation.errors.join('\n ')}`,
439
+ 'AL_DB_MIGRATION_SIMULATION_FAILED'
436
440
  );
437
441
  }
438
442
  logger.info('Migration simulation passed.');
@@ -443,8 +447,9 @@ async function maybeHandleMigrations(dataSource: DataSource) {
443
447
  const simulation = await simulateMigration(dataSource);
444
448
  if (!simulation.success) {
445
449
  logger.error(`Migration simulation failed:\n ${simulation.errors.join('\n ')}`);
446
- throw new Error(
447
- `Migration aborted: simulation failed.\n ${simulation.errors.join('\n ')}`
450
+ throw createCodedError(
451
+ `Migration aborted: simulation failed.\n ${simulation.errors.join('\n ')}`,
452
+ 'AL_DB_MIGRATION_SIMULATION_FAILED'
448
453
  );
449
454
  }
450
455
  logger.info('Migration simulation passed, applying changes...');
@@ -530,7 +535,10 @@ function getDsFunction(
530
535
  case 'sqljs':
531
536
  return makeSqljsDataSource;
532
537
  default:
533
- throw new Error(`Unsupported database type - ${config?.type}`);
538
+ throw createCodedError(
539
+ `Unsupported database type - ${config?.type}`,
540
+ 'AL_DB_UNSUPPORTED_TYPE'
541
+ );
534
542
  }
535
543
  }
536
544
 
@@ -595,7 +603,10 @@ export async function initDatabase(config: DatabaseConfig | undefined) {
595
603
  await initVectorStore(vectEnts, DbContext.getGlobalContext());
596
604
  }
597
605
  } else {
598
- throw new Error(`Unsupported database type - ${getDbType(AppConfig?.store)}`);
606
+ throw createCodedError(
607
+ `Unsupported database type - ${getDbType(AppConfig?.store)}`,
608
+ 'AL_DB_UNSUPPORTED_TYPE_INIT'
609
+ );
599
610
  }
600
611
  }
601
612
  }
@@ -617,9 +628,13 @@ async function insertRowsHelper(
617
628
  ctx: DbContext,
618
629
  doUpsert: boolean
619
630
  ): Promise<void> {
620
- const repo = getDatasourceForTransaction(ctx.txnId).getRepository(tableName);
621
- if (doUpsert) await repo.save(rows);
622
- else await repo.insert(rows);
631
+ try {
632
+ const repo = getDatasourceForTransaction(ctx.txnId).getRepository(tableName);
633
+ if (doUpsert) await repo.save(rows);
634
+ else await repo.insert(rows);
635
+ } catch (err) {
636
+ mapDatabaseError(err);
637
+ }
623
638
  }
624
639
 
625
640
  export async function addRowForFullTextSearch(
@@ -884,7 +899,10 @@ export async function insertRows(
884
899
  }
885
900
  }
886
901
  } else {
887
- throw new UnauthorisedError({ opr: 'insert', entity: tableName });
902
+ throw new UnauthorisedError(
903
+ { opr: 'insert', entity: tableName },
904
+ { agentlangCode: 'AL_DB_INSERT_UNAUTHORIZED' }
905
+ );
888
906
  }
889
907
  }
890
908
 
@@ -926,7 +944,10 @@ export async function insertBetweenRow(
926
944
  const row = Object.fromEntries(attrs);
927
945
  await insertRow(n, row, ctx.clone().setNeedAuthCheck(false), false);
928
946
  } else {
929
- throw new UnauthorisedError({ opr: 'insert', entity: n });
947
+ throw new UnauthorisedError(
948
+ { opr: 'insert', entity: n },
949
+ { agentlangCode: 'AL_DB_BETWEEN_INSERT_UNAUTHORIZED' }
950
+ );
930
951
  }
931
952
  }
932
953
 
@@ -1046,12 +1067,16 @@ export async function updateRow(
1046
1067
  updateObj: object,
1047
1068
  ctx: DbContext
1048
1069
  ): Promise<boolean> {
1049
- await getDatasourceForTransaction(ctx.txnId)
1050
- .createQueryBuilder()
1051
- .update(tableName)
1052
- .set(updateObj)
1053
- .where(objectToWhereClause(queryObj, queryVals), queryVals)
1054
- .execute();
1070
+ try {
1071
+ await getDatasourceForTransaction(ctx.txnId)
1072
+ .createQueryBuilder()
1073
+ .update(tableName)
1074
+ .set(updateObj)
1075
+ .where(objectToWhereClause(queryObj, queryVals), queryVals)
1076
+ .execute();
1077
+ } catch (err) {
1078
+ mapDatabaseError(err);
1079
+ }
1055
1080
  return true;
1056
1081
  }
1057
1082
 
@@ -1069,12 +1094,16 @@ function queryObjectAsWhereClause(qobj: QueryObject): string {
1069
1094
 
1070
1095
  export async function hardDeleteRow(tableName: string, queryObject: QueryObject, ctx: DbContext) {
1071
1096
  const clause = queryObjectAsWhereClause(queryObject);
1072
- await getDatasourceForTransaction(ctx.txnId)
1073
- .createQueryBuilder()
1074
- .delete()
1075
- .from(tableName)
1076
- .where(clause, Object.fromEntries(queryObject))
1077
- .execute();
1097
+ try {
1098
+ await getDatasourceForTransaction(ctx.txnId)
1099
+ .createQueryBuilder()
1100
+ .delete()
1101
+ .from(tableName)
1102
+ .where(clause, Object.fromEntries(queryObject))
1103
+ .execute();
1104
+ } catch (err) {
1105
+ mapDatabaseError(err);
1106
+ }
1078
1107
  return true;
1079
1108
  }
1080
1109
 
@@ -1090,7 +1119,7 @@ function mkBetweenClause(tableName: string | undefined, k: string, queryVals: an
1090
1119
  delete queryVals[k];
1091
1120
  return s;
1092
1121
  } else {
1093
- throw new Error(`between requires an array argument, not ${ov}`);
1122
+ throw createCodedError(`between requires an array argument, not ${ov}`, 'AL_DB_BETWEEN_ARRAY');
1094
1123
  }
1095
1124
  }
1096
1125
 
@@ -1112,7 +1141,10 @@ function objectToWhereClause(queryObj: object, queryVals: any, tableName?: strin
1112
1141
  } else if (op === '<>' || op === '!=') {
1113
1142
  op = 'IS NOT';
1114
1143
  } else {
1115
- throw new Error(`Operator ${op} cannot be appplied to SQL NULL`);
1144
+ throw createCodedError(
1145
+ `Operator ${op} cannot be appplied to SQL NULL`,
1146
+ 'AL_DB_NULL_OPERATOR'
1147
+ );
1116
1148
  }
1117
1149
  }
1118
1150
  const v = isnullcheck ? 'NULL' : `:${k}`;
@@ -1143,7 +1175,10 @@ function objectToRawWhereClause(queryObj: object, queryVals: any, tableName?: st
1143
1175
  } else if (op === '<>' || op === '!=') {
1144
1176
  op = 'IS NOT';
1145
1177
  } else {
1146
- throw new Error(`Operator ${op} cannot be appplied to SQL NULL`);
1178
+ throw createCodedError(
1179
+ `Operator ${op} cannot be appplied to SQL NULL`,
1180
+ 'AL_DB_NULL_OPERATOR'
1181
+ );
1147
1182
  }
1148
1183
  }
1149
1184
  let clause = '';
@@ -1274,8 +1309,12 @@ export async function getMany(
1274
1309
  qb.skip(querySpec.offset);
1275
1310
  }
1276
1311
  qb.where(queryStr, querySpec.queryVals);
1277
- if (hasAggregates) return await qb.getRawMany();
1278
- else return await qb.getMany();
1312
+ try {
1313
+ if (hasAggregates) return await qb.getRawMany();
1314
+ else return await qb.getMany();
1315
+ } catch (err) {
1316
+ mapDatabaseError(err);
1317
+ }
1279
1318
  }
1280
1319
 
1281
1320
  export async function getManyByRawQuery(
@@ -1287,7 +1326,11 @@ export async function getManyByRawQuery(
1287
1326
  .getRepository(tableName)
1288
1327
  .createQueryBuilder();
1289
1328
  qb.where('', querySpec.queryVals);
1290
- return await qb.getMany();
1329
+ try {
1330
+ return await qb.getMany();
1331
+ } catch (err) {
1332
+ mapDatabaseError(err);
1333
+ }
1291
1334
  }
1292
1335
 
1293
1336
  export async function getManyByJoin(
@@ -1348,7 +1391,7 @@ export async function getManyByJoin(
1348
1391
  }
1349
1392
  });
1350
1393
  if (querySpec.intoSpec === undefined) {
1351
- throw new Error('SELECT-INTO pattern is missing');
1394
+ throw createCodedError('SELECT-INTO pattern is missing', 'AL_DB_SELECT_INTO_MISSING');
1352
1395
  }
1353
1396
  const intos = querySpec.intoSpec.size > 0 ? intoSpecToSql(querySpec.intoSpec) : '';
1354
1397
  const intos_sep = intos.length === 0 ? '' : ',';
@@ -1370,7 +1413,11 @@ export async function getManyByJoin(
1370
1413
  }
1371
1414
  logger.debug(`Join Query: ${sql}`);
1372
1415
  const qb = getDatasourceForTransaction(ctx.txnId).getRepository(tableName).manager;
1373
- return await qb.query(sql);
1416
+ try {
1417
+ return await qb.query(sql);
1418
+ } catch (err) {
1419
+ mapDatabaseError(err);
1420
+ }
1374
1421
  }
1375
1422
 
1376
1423
  function intoSpecToSql(intoSpec: Map<string, string>): string {
@@ -1440,7 +1487,11 @@ export async function getAllConnected(
1440
1487
  connAlias,
1441
1488
  buildQueryFromConnnectionInfo(connAlias, alias, connInfo)
1442
1489
  );
1443
- return await qb.getRawMany();
1490
+ try {
1491
+ return await qb.getRawMany();
1492
+ } catch (err) {
1493
+ mapDatabaseError(err);
1494
+ }
1444
1495
  }
1445
1496
 
1446
1497
  const transactionsDb: Map<string, QueryRunner> = new Map<string, QueryRunner>();
@@ -1453,7 +1504,7 @@ export async function startDbTransaction(): Promise<string> {
1453
1504
  transactionsDb.set(txnId, queryRunner);
1454
1505
  return txnId;
1455
1506
  } else {
1456
- throw new Error('Database not initialized');
1507
+ throw createCodedError('Database not initialized', 'AL_DB_NOT_INITIALIZED');
1457
1508
  }
1458
1509
  }
1459
1510
 
@@ -1461,13 +1512,13 @@ function getDatasourceForTransaction(txnId: string | undefined): DataSource | En
1461
1512
  if (txnId) {
1462
1513
  const qr: QueryRunner | undefined = transactionsDb.get(txnId);
1463
1514
  if (qr === undefined) {
1464
- throw new Error(`Transaction not found - ${txnId}`);
1515
+ throw createCodedError(`Transaction not found - ${txnId}`, 'AL_DB_TXN_NOT_FOUND');
1465
1516
  } else {
1466
1517
  return qr.manager;
1467
1518
  }
1468
1519
  } else {
1469
1520
  if (defaultDataSource !== undefined) return defaultDataSource;
1470
- else throw new Error('No default datasource is initialized');
1521
+ else throw createCodedError('No default datasource is initialized', 'AL_DB_NO_DATASOURCE');
1471
1522
  }
1472
1523
  }
1473
1524