@mindstudio-ai/agent 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,18 @@ var MindStudioError = class extends Error {
7
7
  this.details = details;
8
8
  }
9
9
  name = "MindStudioError";
10
+ toString() {
11
+ return `MindStudioError [${this.code}] (${this.status}): ${this.message}`;
12
+ }
13
+ toJSON() {
14
+ return {
15
+ name: this.name,
16
+ message: this.message,
17
+ code: this.code,
18
+ status: this.status,
19
+ ...this.details != null && { details: this.details }
20
+ };
21
+ }
10
22
  };
11
23
 
12
24
  // src/http.ts
@@ -52,7 +64,10 @@ async function requestWithRetry(config, method, url, body, attempt) {
52
64
  }
53
65
  if (body2.code) code = body2.code;
54
66
  } catch {
55
- if (text && text.length < 500) message = text;
67
+ if (text) {
68
+ const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
69
+ if (stripped) message = stripped.slice(0, 200);
70
+ }
56
71
  }
57
72
  } catch {
58
73
  }
@@ -85,7 +100,7 @@ var RateLimiter = class {
85
100
  async acquire() {
86
101
  if (this.callCount >= this.callCap) {
87
102
  throw new MindStudioError(
88
- `Call cap reached (${this.callCap} calls). Internal tokens are limited to 500 calls per execution.`,
103
+ `Call cap exceeded (${this.callCap} calls per execution). Reduce the number of API calls or use executeStepBatch() to combine multiple steps.`,
89
104
  "call_cap_exceeded",
90
105
  429
91
106
  );
@@ -152,7 +167,7 @@ function loadConfig() {
152
167
 
153
168
  // src/auth/index.ts
154
169
  var AuthContext = class {
155
- /** The current user's ID. */
170
+ /** The current user's ID, or null for unauthenticated users. */
156
171
  userId;
157
172
  /** The current user's roles in this app. */
158
173
  roles;
@@ -191,9 +206,16 @@ var AuthContext = class {
191
206
  * ```
192
207
  */
193
208
  requireRole(...roles) {
209
+ if (this.userId == null) {
210
+ throw new MindStudioError(
211
+ "No authenticated user",
212
+ "unauthenticated",
213
+ 401
214
+ );
215
+ }
194
216
  if (!this.hasRole(...roles)) {
195
217
  throw new MindStudioError(
196
- `User does not have required role: ${roles.join(", ")}`,
218
+ `User has role(s) [${this.roles.join(", ") || "none"}] but requires one of: [${roles.join(", ")}]`,
197
219
  "forbidden",
198
220
  403
199
221
  );
@@ -259,6 +281,11 @@ function deserializeRow(row, columns) {
259
281
  } catch {
260
282
  result[key] = value;
261
283
  }
284
+ } else if (col?.type === "boolean" && typeof value === "number") {
285
+ result[key] = value !== 0;
286
+ } else if (col?.type === "number" && typeof value === "string") {
287
+ const num = Number(value);
288
+ result[key] = Number.isNaN(num) ? value : num;
262
289
  } else {
263
290
  result[key] = value;
264
291
  }
@@ -277,11 +304,6 @@ function buildSelect(table, options = {}) {
277
304
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
278
305
  return { sql, params: params.length > 0 ? params : void 0 };
279
306
  }
280
- function buildCount(table, where, whereParams) {
281
- let sql = `SELECT COUNT(*) as count FROM ${table}`;
282
- if (where) sql += ` WHERE ${where}`;
283
- return { sql, params: whereParams?.length ? whereParams : void 0 };
284
- }
285
307
  function buildExists(table, where, whereParams, negate) {
286
308
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
287
309
  const fn = negate ? "NOT EXISTS" : "EXISTS";
@@ -352,20 +374,20 @@ function compilePredicate(fn) {
352
374
  try {
353
375
  const source = fn.toString();
354
376
  const paramName = extractParamName(source);
355
- if (!paramName) return { type: "js", fn };
377
+ if (!paramName) return { type: "js", fn, reason: "could not extract parameter name" };
356
378
  const body = extractBody(source);
357
- if (!body) return { type: "js", fn };
379
+ if (!body) return { type: "js", fn, reason: "could not extract function body" };
358
380
  const tokens = tokenize(body);
359
- if (tokens.length === 0) return { type: "js", fn };
381
+ if (tokens.length === 0) return { type: "js", fn, reason: "empty token stream" };
360
382
  const parser = new Parser(tokens, paramName, fn);
361
383
  const ast = parser.parseExpression();
362
- if (!ast) return { type: "js", fn };
363
- if (parser.pos < tokens.length) return { type: "js", fn };
384
+ if (!ast) return { type: "js", fn, reason: "could not parse expression" };
385
+ if (parser.pos < tokens.length) return { type: "js", fn, reason: "unexpected tokens after expression" };
364
386
  const where = compileNode(ast);
365
- if (!where) return { type: "js", fn };
387
+ if (!where) return { type: "js", fn, reason: "could not compile to SQL" };
366
388
  return { type: "sql", where };
367
- } catch {
368
- return { type: "js", fn };
389
+ } catch (err) {
390
+ return { type: "js", fn, reason: `compilation error: ${err?.message || "unknown"}` };
369
391
  }
370
392
  }
371
393
  function extractParamName(source) {
@@ -842,6 +864,9 @@ var Query = class _Query {
842
864
  _limit;
843
865
  _offset;
844
866
  _config;
867
+ /** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
868
+ _rawWhere;
869
+ _rawWhereParams;
845
870
  /** @internal Post-process transform applied after row deserialization. */
846
871
  _postProcess;
847
872
  constructor(config, options) {
@@ -852,6 +877,8 @@ var Query = class _Query {
852
877
  this._limit = options?.limit;
853
878
  this._offset = options?.offset;
854
879
  this._postProcess = options?.postProcess;
880
+ this._rawWhere = options?.rawWhere;
881
+ this._rawWhereParams = options?.rawWhereParams;
855
882
  }
856
883
  _clone(overrides) {
857
884
  return new _Query(this._config, {
@@ -860,7 +887,9 @@ var Query = class _Query {
860
887
  reversed: overrides.reversed ?? this._reversed,
861
888
  limit: overrides.limit ?? this._limit,
862
889
  offset: overrides.offset ?? this._offset,
863
- postProcess: overrides.postProcess
890
+ postProcess: overrides.postProcess,
891
+ rawWhere: this._rawWhere,
892
+ rawWhereParams: this._rawWhereParams
864
893
  });
865
894
  }
866
895
  // -------------------------------------------------------------------------
@@ -897,33 +926,16 @@ var Query = class _Query {
897
926
  postProcess: (rows) => rows[0] ?? null
898
927
  });
899
928
  }
900
- async count() {
901
- const compiled = this._compilePredicates();
902
- if (compiled.allSql) {
903
- const query = buildCount(
904
- this._config.tableName,
905
- compiled.sqlWhere || void 0
906
- );
907
- const results = await this._config.executeBatch([query]);
908
- const row = results[0]?.rows[0];
909
- return row?.count ?? 0;
910
- }
911
- const rows = await this._fetchAndFilterInJs(compiled);
912
- return rows.length;
929
+ count() {
930
+ return this._clone({
931
+ postProcess: (rows) => rows.length
932
+ });
913
933
  }
914
- async some() {
915
- const compiled = this._compilePredicates();
916
- if (compiled.allSql) {
917
- const query = buildExists(
918
- this._config.tableName,
919
- compiled.sqlWhere || void 0
920
- );
921
- const results = await this._config.executeBatch([query]);
922
- const row = results[0]?.rows[0];
923
- return row?.result === 1;
924
- }
925
- const rows = await this._fetchAndFilterInJs(compiled);
926
- return rows.length > 0;
934
+ some() {
935
+ return this._clone({
936
+ limit: 1,
937
+ postProcess: (rows) => rows.length > 0
938
+ });
927
939
  }
928
940
  async every() {
929
941
  const compiled = this._compilePredicates();
@@ -950,19 +962,19 @@ var Query = class _Query {
950
962
  max(accessor) {
951
963
  return this.sortBy(accessor).reverse().first();
952
964
  }
953
- async groupBy(accessor) {
954
- const rows = await this._execute();
955
- const map = /* @__PURE__ */ new Map();
956
- for (const row of rows) {
957
- const key = accessor(row);
958
- const group = map.get(key);
959
- if (group) {
960
- group.push(row);
961
- } else {
962
- map.set(key, [row]);
965
+ groupBy(accessor) {
966
+ return this._clone({
967
+ postProcess: (rows) => {
968
+ const map = /* @__PURE__ */ new Map();
969
+ for (const row of rows) {
970
+ const key = accessor(row);
971
+ const group = map.get(key);
972
+ if (group) group.push(row);
973
+ else map.set(key, [row]);
974
+ }
975
+ return map;
963
976
  }
964
- }
965
- return map;
977
+ });
966
978
  }
967
979
  // -------------------------------------------------------------------------
968
980
  // Batch compilation — used by db.batch() to extract SQL without executing
@@ -976,6 +988,16 @@ var Query = class _Query {
976
988
  * all rows and this query can filter them in JS post-fetch.
977
989
  */
978
990
  _compile() {
991
+ if (this._rawWhere) {
992
+ const query = buildSelect(this._config.tableName, {
993
+ where: this._rawWhere,
994
+ whereParams: this._rawWhereParams,
995
+ orderBy: void 0,
996
+ limit: this._limit,
997
+ offset: this._offset
998
+ });
999
+ return { type: "query", query, fallbackQuery: null, config: this._config, postProcess: this._postProcess };
1000
+ }
979
1001
  const compiled = this._compilePredicates();
980
1002
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
981
1003
  if (compiled.allSql) {
@@ -1054,6 +1076,21 @@ var Query = class _Query {
1054
1076
  // Execution internals
1055
1077
  // -------------------------------------------------------------------------
1056
1078
  async _execute() {
1079
+ if (this._rawWhere) {
1080
+ const query = buildSelect(this._config.tableName, {
1081
+ where: this._rawWhere,
1082
+ whereParams: this._rawWhereParams,
1083
+ limit: this._limit,
1084
+ offset: this._offset
1085
+ });
1086
+ const results = await this._config.executeBatch([query]);
1087
+ return results[0].rows.map(
1088
+ (row) => deserializeRow(
1089
+ row,
1090
+ this._config.columns
1091
+ )
1092
+ );
1093
+ }
1057
1094
  const compiled = this._compilePredicates();
1058
1095
  if (compiled.allSql) {
1059
1096
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
@@ -1104,9 +1141,12 @@ var Query = class _Query {
1104
1141
  }
1105
1142
  async _fetchAndFilterInJs(compiled) {
1106
1143
  const allRows = await this._fetchAllRows();
1107
- if (compiled.compiled.some((c) => c.type === "js")) {
1144
+ const jsFallbacks = compiled.compiled.filter((c) => c.type === "js");
1145
+ if (jsFallbacks.length > 0) {
1146
+ const reasons = jsFallbacks.map((c) => c.type === "js" ? c.reason : void 0).filter(Boolean);
1147
+ const reasonSuffix = reasons.length > 0 ? ` (${reasons.join("; ")})` : "";
1108
1148
  console.warn(
1109
- `[mindstudio] Filter on ${this._config.tableName} could not be compiled to SQL \u2014 scanning ${allRows.length} rows in JS`
1149
+ `[mindstudio] Filter on '${this._config.tableName}' could not be compiled to SQL${reasonSuffix} \u2014 scanning ${allRows.length} rows in JS`
1110
1150
  );
1111
1151
  }
1112
1152
  return allRows.filter(
@@ -1179,8 +1219,10 @@ var Mutation = class _Mutation {
1179
1219
  */
1180
1220
  _compile() {
1181
1221
  if (this._executor) {
1182
- throw new Error(
1183
- "This operation cannot be batched (e.g. removeAll with a predicate that cannot compile to SQL). Await it separately."
1222
+ throw new MindStudioError(
1223
+ "This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
1224
+ "not_batchable",
1225
+ 400
1184
1226
  );
1185
1227
  }
1186
1228
  return {
@@ -1217,58 +1259,61 @@ var Table = class {
1217
1259
  this._config = config;
1218
1260
  }
1219
1261
  // -------------------------------------------------------------------------
1220
- // Reads — direct
1262
+ // Reads — all return batchable Query objects (lazy until awaited)
1221
1263
  // -------------------------------------------------------------------------
1222
- async get(id) {
1223
- const query = buildSelect(this._config.tableName, {
1224
- where: `id = ?`,
1225
- whereParams: [id],
1226
- limit: 1
1264
+ /** Get a single row by ID. Returns null if not found. */
1265
+ get(id) {
1266
+ return new Query(this._config, {
1267
+ rawWhere: "id = ?",
1268
+ rawWhereParams: [id],
1269
+ limit: 1,
1270
+ postProcess: (rows) => rows[0] ?? null
1227
1271
  });
1228
- const results = await this._config.executeBatch([query]);
1229
- if (results[0].rows.length === 0) return null;
1230
- return deserializeRow(
1231
- results[0].rows[0],
1232
- this._config.columns
1233
- );
1234
1272
  }
1235
- async findOne(predicate) {
1273
+ /** Find the first row matching a predicate. Returns null if none match. */
1274
+ findOne(predicate) {
1236
1275
  return this.filter(predicate).first();
1237
1276
  }
1238
- async count(predicate) {
1277
+ count(predicate) {
1239
1278
  if (predicate) return this.filter(predicate).count();
1240
- const query = buildCount(this._config.tableName);
1241
- const results = await this._config.executeBatch([query]);
1242
- const row = results[0]?.rows[0];
1243
- return row?.count ?? 0;
1279
+ return this.toArray().count();
1244
1280
  }
1245
- async some(predicate) {
1281
+ /** True if any row matches the predicate. */
1282
+ some(predicate) {
1246
1283
  return this.filter(predicate).some();
1247
1284
  }
1285
+ /** True if all rows match the predicate. */
1248
1286
  async every(predicate) {
1249
1287
  return this.filter(predicate).every();
1250
1288
  }
1289
+ /** True if the table has zero rows. */
1251
1290
  async isEmpty() {
1252
1291
  const query = buildExists(this._config.tableName, void 0, void 0, true);
1253
1292
  const results = await this._config.executeBatch([query]);
1254
1293
  const row = results[0]?.rows[0];
1255
1294
  return row?.result === 1;
1256
1295
  }
1257
- async min(accessor) {
1296
+ /** Row with the minimum value for a field, or null if table is empty. */
1297
+ min(accessor) {
1258
1298
  return this.sortBy(accessor).first();
1259
1299
  }
1260
- async max(accessor) {
1300
+ /** Row with the maximum value for a field, or null if table is empty. */
1301
+ max(accessor) {
1261
1302
  return this.sortBy(accessor).reverse().first();
1262
1303
  }
1263
- async groupBy(accessor) {
1304
+ /** Group rows by a field. Returns a Map. */
1305
+ groupBy(accessor) {
1264
1306
  return new Query(this._config).groupBy(accessor);
1265
1307
  }
1266
- // -------------------------------------------------------------------------
1267
- // Reads — chainable
1268
- // -------------------------------------------------------------------------
1308
+ /** Get all rows as an array. */
1309
+ toArray() {
1310
+ return new Query(this._config);
1311
+ }
1312
+ /** Filter rows by a predicate. Returns a chainable Query. */
1269
1313
  filter(predicate) {
1270
1314
  return new Query(this._config).filter(predicate);
1271
1315
  }
1316
+ /** Sort rows by a field. Returns a chainable Query. */
1272
1317
  sortBy(accessor) {
1273
1318
  return new Query(this._config).sortBy(accessor);
1274
1319
  }
@@ -1295,7 +1340,11 @@ var Table = class {
1295
1340
  this._config.columns
1296
1341
  );
1297
1342
  }
1298
- return void 0;
1343
+ throw new MindStudioError(
1344
+ `Insert into '${this._config.tableName}' succeeded but returned no row. This may indicate a constraint violation.`,
1345
+ "insert_failed",
1346
+ 500
1347
+ );
1299
1348
  });
1300
1349
  const result = isArray ? rows : rows[0];
1301
1350
  this._syncRolesIfNeeded(
@@ -1319,6 +1368,13 @@ var Table = class {
1319
1368
  this._config.columns
1320
1369
  );
1321
1370
  return new Mutation(this._config, [query], (results) => {
1371
+ if (!results[0]?.rows[0]) {
1372
+ throw new MindStudioError(
1373
+ `Row not found: no row with ID '${id}' in table '${this._config.tableName}'`,
1374
+ "row_not_found",
1375
+ 404
1376
+ );
1377
+ }
1322
1378
  const result = deserializeRow(
1323
1379
  results[0].rows[0],
1324
1380
  this._config.columns
@@ -1333,7 +1389,9 @@ var Table = class {
1333
1389
  }
1334
1390
  remove(id) {
1335
1391
  const query = buildDelete(this._config.tableName, `id = ?`, [id]);
1336
- return new Mutation(this._config, [query], () => void 0);
1392
+ return new Mutation(this._config, [query], (results) => ({
1393
+ deleted: results[0].changes > 0
1394
+ }));
1337
1395
  }
1338
1396
  /**
1339
1397
  * Remove all rows matching a predicate. Returns the count removed.
@@ -1367,7 +1425,7 @@ var Table = class {
1367
1425
  }
1368
1426
  clear() {
1369
1427
  const query = buildDelete(this._config.tableName);
1370
- return new Mutation(this._config, [query], () => void 0);
1428
+ return new Mutation(this._config, [query], (results) => results[0].changes);
1371
1429
  }
1372
1430
  /**
1373
1431
  * Insert a row, or update it if a row with the same unique key already
@@ -1385,6 +1443,15 @@ var Table = class {
1385
1443
  this._validateUniqueConstraint(conflictColumns);
1386
1444
  const withDefaults = this._config.defaults ? { ...this._config.defaults, ...data } : data;
1387
1445
  this._checkManagedColumns(withDefaults);
1446
+ for (const col of conflictColumns) {
1447
+ if (!(col in withDefaults)) {
1448
+ throw new MindStudioError(
1449
+ `Upsert on ${this._config.tableName} requires "${col}" in data (conflict key)`,
1450
+ "missing_conflict_key",
1451
+ 400
1452
+ );
1453
+ }
1454
+ }
1388
1455
  const query = buildUpsert(
1389
1456
  this._config.tableName,
1390
1457
  withDefaults,
@@ -1392,6 +1459,13 @@ var Table = class {
1392
1459
  this._config.columns
1393
1460
  );
1394
1461
  return new Mutation(this._config, [query], (results) => {
1462
+ if (!results[0]?.rows[0]) {
1463
+ throw new MindStudioError(
1464
+ `Upsert into ${this._config.tableName} returned no row`,
1465
+ "upsert_failed",
1466
+ 500
1467
+ );
1468
+ }
1395
1469
  const result = deserializeRow(
1396
1470
  results[0].rows[0],
1397
1471
  this._config.columns
@@ -1543,7 +1617,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1543
1617
  if (c.type === "query") {
1544
1618
  if (!c.query && c.predicates?.length) {
1545
1619
  console.warn(
1546
- `[mindstudio] db.batch(): filter on ${c.config.tableName} could not be compiled to SQL \u2014 processing in JS`
1620
+ `[mindstudio] db.batch(): filter on '${c.config.tableName}' could not be compiled to SQL \u2014 processing in JS`
1547
1621
  );
1548
1622
  }
1549
1623
  return Query._processResults(results[0], c);
@@ -3018,9 +3092,14 @@ var stepMetadata = {
3018
3092
  },
3019
3093
  "sendEmail": {
3020
3094
  stepType: "sendEmail",
3021
- description: "Send an email to one or more configured recipient addresses.",
3022
- usageNotes: '- Recipient email addresses are resolved from OAuth connections configured by the app creator. The user running the workflow does not specify the recipient directly.\n- If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.\n- When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.\n- connectionId can be a comma-separated list to send to multiple recipients.\n- The special connectionId "trigger_email" uses the email address that triggered the workflow.',
3023
- inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "type": "string" }, "description": "URLs of files to attach to the email" } }, "required": ["subject", "body"] },
3095
+ description: "Send an email to one or more recipient addresses.",
3096
+ usageNotes: `- Use the "to" field to send to specific email addresses directly. For v2 apps, recipients must be verified users in the app's user table.
3097
+ - Alternatively, recipient email addresses can be resolved from OAuth connections configured by the app creator via connectionId. The user running the workflow does not specify the recipient directly.
3098
+ - If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.
3099
+ - When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.
3100
+ - connectionId can be a comma-separated list to send to multiple recipients.
3101
+ - The special connectionId "trigger_email" uses the email address that triggered the workflow.`,
3102
+ inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "to": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "type": "string" }, "description": "URLs of files to attach to the email" } }, "required": ["subject", "body"] },
3024
3103
  outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "Email addresses the message was sent to" } }, "required": ["recipients"] }
3025
3104
  },
3026
3105
  "sendGmailDraft": {
@@ -3342,7 +3421,7 @@ var MindStudioAgent = class {
3342
3421
  const res = await fetch(data.outputUrl);
3343
3422
  if (!res.ok) {
3344
3423
  throw new MindStudioError(
3345
- `Failed to fetch output from S3: ${res.status} ${res.statusText}`,
3424
+ `Failed to fetch ${stepType} output from S3: ${res.status} ${res.statusText}`,
3346
3425
  "output_fetch_error",
3347
3426
  res.status
3348
3427
  );
@@ -3405,13 +3484,27 @@ var MindStudioAgent = class {
3405
3484
  this._httpConfig.rateLimiter.updateFromHeaders(res.headers);
3406
3485
  if (!res.ok) {
3407
3486
  this._httpConfig.rateLimiter.release();
3408
- const errorBody = await res.json().catch(() => ({}));
3409
- throw new MindStudioError(
3410
- errorBody.message || `${res.status} ${res.statusText}`,
3411
- errorBody.code || "api_error",
3412
- res.status,
3413
- errorBody
3414
- );
3487
+ let message = `${res.status} ${res.statusText}`;
3488
+ let code = "api_error";
3489
+ let details;
3490
+ try {
3491
+ const text = await res.text();
3492
+ try {
3493
+ const body2 = JSON.parse(text);
3494
+ details = body2;
3495
+ const errMsg = (typeof body2.error === "string" ? body2.error : void 0) ?? (typeof body2.message === "string" ? body2.message : void 0) ?? (typeof body2.details === "string" ? body2.details : void 0);
3496
+ if (errMsg) message = errMsg;
3497
+ else if (body2.error || body2.message || body2.details) {
3498
+ message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
3499
+ }
3500
+ if (body2.code) code = body2.code;
3501
+ } catch {
3502
+ const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
3503
+ if (stripped) message = stripped.slice(0, 200);
3504
+ }
3505
+ } catch {
3506
+ }
3507
+ throw new MindStudioError(`[${stepType}] ${message}`, code, res.status, details);
3415
3508
  }
3416
3509
  const headers = res.headers;
3417
3510
  try {
@@ -3444,7 +3537,7 @@ var MindStudioAgent = class {
3444
3537
  };
3445
3538
  } else if (event.type === "error") {
3446
3539
  throw new MindStudioError(
3447
- event.error || "Step execution failed",
3540
+ `[${stepType}] ${event.error || "Step execution failed"}`,
3448
3541
  "step_error",
3449
3542
  500
3450
3543
  );
@@ -3483,7 +3576,7 @@ var MindStudioAgent = class {
3483
3576
  }
3484
3577
  if (!doneEvent) {
3485
3578
  throw new MindStudioError(
3486
- "Stream ended without a done event",
3579
+ `[${stepType}] Stream ended unexpectedly without completing. The step execution may have been interrupted.`,
3487
3580
  "stream_error",
3488
3581
  500
3489
3582
  );
@@ -3495,7 +3588,7 @@ var MindStudioAgent = class {
3495
3588
  const s3Res = await fetch(doneEvent.outputUrl);
3496
3589
  if (!s3Res.ok) {
3497
3590
  throw new MindStudioError(
3498
- `Failed to fetch output from S3: ${s3Res.status} ${s3Res.statusText}`,
3591
+ `Failed to fetch ${stepType} output from S3: ${s3Res.status} ${s3Res.statusText}`,
3499
3592
  "output_fetch_error",
3500
3593
  s3Res.status
3501
3594
  );
@@ -3856,7 +3949,7 @@ var MindStudioAgent = class {
3856
3949
  }
3857
3950
  if (!this._auth) {
3858
3951
  throw new MindStudioError(
3859
- "Auth context not yet loaded. Call `await agent.ensureContext()` or perform any db operation first (which auto-hydrates context). Inside the MindStudio sandbox, context is loaded automatically.",
3952
+ "Auth context not loaded. Call `await agent.ensureContext()` first, or perform any db operation (which auto-loads context).",
3860
3953
  "context_not_loaded",
3861
3954
  400
3862
3955
  );
@@ -4030,13 +4123,12 @@ var MindStudioAgent = class {
4030
4123
  if (!res.ok) {
4031
4124
  const text = await res.text().catch(() => "");
4032
4125
  console.warn(
4033
- `[mindstudio] Failed to sync roles for user ${userId}: ${res.status} ${text}`
4126
+ `[mindstudio] Role sync failed for user ${userId} (${res.status}${text ? ": " + text.slice(0, 100) : ""}). Roles were saved to the database but may not be reflected in auth.hasRole() until the next successful write.`
4034
4127
  );
4035
4128
  }
4036
4129
  } catch (err) {
4037
4130
  console.warn(
4038
- `[mindstudio] Failed to sync roles for user ${userId}:`,
4039
- err
4131
+ `[mindstudio] Role sync failed for user ${userId}: network error. Roles were saved to the database but may not be reflected in auth.hasRole() until the next successful write.`
4040
4132
  );
4041
4133
  }
4042
4134
  }