@mindstudio-ai/agent 0.1.36 → 0.1.37

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
  );
@@ -277,11 +299,6 @@ function buildSelect(table, options = {}) {
277
299
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
278
300
  return { sql, params: params.length > 0 ? params : void 0 };
279
301
  }
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
302
  function buildExists(table, where, whereParams, negate) {
286
303
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
287
304
  const fn = negate ? "NOT EXISTS" : "EXISTS";
@@ -352,20 +369,20 @@ function compilePredicate(fn) {
352
369
  try {
353
370
  const source = fn.toString();
354
371
  const paramName = extractParamName(source);
355
- if (!paramName) return { type: "js", fn };
372
+ if (!paramName) return { type: "js", fn, reason: "could not extract parameter name" };
356
373
  const body = extractBody(source);
357
- if (!body) return { type: "js", fn };
374
+ if (!body) return { type: "js", fn, reason: "could not extract function body" };
358
375
  const tokens = tokenize(body);
359
- if (tokens.length === 0) return { type: "js", fn };
376
+ if (tokens.length === 0) return { type: "js", fn, reason: "empty token stream" };
360
377
  const parser = new Parser(tokens, paramName, fn);
361
378
  const ast = parser.parseExpression();
362
- if (!ast) return { type: "js", fn };
363
- if (parser.pos < tokens.length) return { type: "js", fn };
379
+ if (!ast) return { type: "js", fn, reason: "could not parse expression" };
380
+ if (parser.pos < tokens.length) return { type: "js", fn, reason: "unexpected tokens after expression" };
364
381
  const where = compileNode(ast);
365
- if (!where) return { type: "js", fn };
382
+ if (!where) return { type: "js", fn, reason: "could not compile to SQL" };
366
383
  return { type: "sql", where };
367
- } catch {
368
- return { type: "js", fn };
384
+ } catch (err) {
385
+ return { type: "js", fn, reason: `compilation error: ${err?.message || "unknown"}` };
369
386
  }
370
387
  }
371
388
  function extractParamName(source) {
@@ -842,6 +859,9 @@ var Query = class _Query {
842
859
  _limit;
843
860
  _offset;
844
861
  _config;
862
+ /** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
863
+ _rawWhere;
864
+ _rawWhereParams;
845
865
  /** @internal Post-process transform applied after row deserialization. */
846
866
  _postProcess;
847
867
  constructor(config, options) {
@@ -852,6 +872,8 @@ var Query = class _Query {
852
872
  this._limit = options?.limit;
853
873
  this._offset = options?.offset;
854
874
  this._postProcess = options?.postProcess;
875
+ this._rawWhere = options?.rawWhere;
876
+ this._rawWhereParams = options?.rawWhereParams;
855
877
  }
856
878
  _clone(overrides) {
857
879
  return new _Query(this._config, {
@@ -860,7 +882,9 @@ var Query = class _Query {
860
882
  reversed: overrides.reversed ?? this._reversed,
861
883
  limit: overrides.limit ?? this._limit,
862
884
  offset: overrides.offset ?? this._offset,
863
- postProcess: overrides.postProcess
885
+ postProcess: overrides.postProcess,
886
+ rawWhere: this._rawWhere,
887
+ rawWhereParams: this._rawWhereParams
864
888
  });
865
889
  }
866
890
  // -------------------------------------------------------------------------
@@ -897,33 +921,16 @@ var Query = class _Query {
897
921
  postProcess: (rows) => rows[0] ?? null
898
922
  });
899
923
  }
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;
924
+ count() {
925
+ return this._clone({
926
+ postProcess: (rows) => rows.length
927
+ });
913
928
  }
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;
929
+ some() {
930
+ return this._clone({
931
+ limit: 1,
932
+ postProcess: (rows) => rows.length > 0
933
+ });
927
934
  }
928
935
  async every() {
929
936
  const compiled = this._compilePredicates();
@@ -950,19 +957,19 @@ var Query = class _Query {
950
957
  max(accessor) {
951
958
  return this.sortBy(accessor).reverse().first();
952
959
  }
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]);
960
+ groupBy(accessor) {
961
+ return this._clone({
962
+ postProcess: (rows) => {
963
+ const map = /* @__PURE__ */ new Map();
964
+ for (const row of rows) {
965
+ const key = accessor(row);
966
+ const group = map.get(key);
967
+ if (group) group.push(row);
968
+ else map.set(key, [row]);
969
+ }
970
+ return map;
963
971
  }
964
- }
965
- return map;
972
+ });
966
973
  }
967
974
  // -------------------------------------------------------------------------
968
975
  // Batch compilation — used by db.batch() to extract SQL without executing
@@ -976,6 +983,16 @@ var Query = class _Query {
976
983
  * all rows and this query can filter them in JS post-fetch.
977
984
  */
978
985
  _compile() {
986
+ if (this._rawWhere) {
987
+ const query = buildSelect(this._config.tableName, {
988
+ where: this._rawWhere,
989
+ whereParams: this._rawWhereParams,
990
+ orderBy: void 0,
991
+ limit: this._limit,
992
+ offset: this._offset
993
+ });
994
+ return { type: "query", query, fallbackQuery: null, config: this._config, postProcess: this._postProcess };
995
+ }
979
996
  const compiled = this._compilePredicates();
980
997
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
981
998
  if (compiled.allSql) {
@@ -1054,6 +1071,21 @@ var Query = class _Query {
1054
1071
  // Execution internals
1055
1072
  // -------------------------------------------------------------------------
1056
1073
  async _execute() {
1074
+ if (this._rawWhere) {
1075
+ const query = buildSelect(this._config.tableName, {
1076
+ where: this._rawWhere,
1077
+ whereParams: this._rawWhereParams,
1078
+ limit: this._limit,
1079
+ offset: this._offset
1080
+ });
1081
+ const results = await this._config.executeBatch([query]);
1082
+ return results[0].rows.map(
1083
+ (row) => deserializeRow(
1084
+ row,
1085
+ this._config.columns
1086
+ )
1087
+ );
1088
+ }
1057
1089
  const compiled = this._compilePredicates();
1058
1090
  if (compiled.allSql) {
1059
1091
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
@@ -1104,9 +1136,12 @@ var Query = class _Query {
1104
1136
  }
1105
1137
  async _fetchAndFilterInJs(compiled) {
1106
1138
  const allRows = await this._fetchAllRows();
1107
- if (compiled.compiled.some((c) => c.type === "js")) {
1139
+ const jsFallbacks = compiled.compiled.filter((c) => c.type === "js");
1140
+ if (jsFallbacks.length > 0) {
1141
+ const reasons = jsFallbacks.map((c) => c.type === "js" ? c.reason : void 0).filter(Boolean);
1142
+ const reasonSuffix = reasons.length > 0 ? ` (${reasons.join("; ")})` : "";
1108
1143
  console.warn(
1109
- `[mindstudio] Filter on ${this._config.tableName} could not be compiled to SQL \u2014 scanning ${allRows.length} rows in JS`
1144
+ `[mindstudio] Filter on '${this._config.tableName}' could not be compiled to SQL${reasonSuffix} \u2014 scanning ${allRows.length} rows in JS`
1110
1145
  );
1111
1146
  }
1112
1147
  return allRows.filter(
@@ -1179,8 +1214,10 @@ var Mutation = class _Mutation {
1179
1214
  */
1180
1215
  _compile() {
1181
1216
  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."
1217
+ throw new MindStudioError(
1218
+ "This operation cannot be batched (e.g. removeAll with a JS-fallback predicate). Await it separately instead of passing to db.batch().",
1219
+ "not_batchable",
1220
+ 400
1184
1221
  );
1185
1222
  }
1186
1223
  return {
@@ -1217,58 +1254,61 @@ var Table = class {
1217
1254
  this._config = config;
1218
1255
  }
1219
1256
  // -------------------------------------------------------------------------
1220
- // Reads — direct
1257
+ // Reads — all return batchable Query objects (lazy until awaited)
1221
1258
  // -------------------------------------------------------------------------
1222
- async get(id) {
1223
- const query = buildSelect(this._config.tableName, {
1224
- where: `id = ?`,
1225
- whereParams: [id],
1226
- limit: 1
1259
+ /** Get a single row by ID. Returns null if not found. */
1260
+ get(id) {
1261
+ return new Query(this._config, {
1262
+ rawWhere: "id = ?",
1263
+ rawWhereParams: [id],
1264
+ limit: 1,
1265
+ postProcess: (rows) => rows[0] ?? null
1227
1266
  });
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
1267
  }
1235
- async findOne(predicate) {
1268
+ /** Find the first row matching a predicate. Returns null if none match. */
1269
+ findOne(predicate) {
1236
1270
  return this.filter(predicate).first();
1237
1271
  }
1238
- async count(predicate) {
1272
+ count(predicate) {
1239
1273
  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;
1274
+ return this.toArray().count();
1244
1275
  }
1245
- async some(predicate) {
1276
+ /** True if any row matches the predicate. */
1277
+ some(predicate) {
1246
1278
  return this.filter(predicate).some();
1247
1279
  }
1280
+ /** True if all rows match the predicate. */
1248
1281
  async every(predicate) {
1249
1282
  return this.filter(predicate).every();
1250
1283
  }
1284
+ /** True if the table has zero rows. */
1251
1285
  async isEmpty() {
1252
1286
  const query = buildExists(this._config.tableName, void 0, void 0, true);
1253
1287
  const results = await this._config.executeBatch([query]);
1254
1288
  const row = results[0]?.rows[0];
1255
1289
  return row?.result === 1;
1256
1290
  }
1257
- async min(accessor) {
1291
+ /** Row with the minimum value for a field, or null if table is empty. */
1292
+ min(accessor) {
1258
1293
  return this.sortBy(accessor).first();
1259
1294
  }
1260
- async max(accessor) {
1295
+ /** Row with the maximum value for a field, or null if table is empty. */
1296
+ max(accessor) {
1261
1297
  return this.sortBy(accessor).reverse().first();
1262
1298
  }
1263
- async groupBy(accessor) {
1299
+ /** Group rows by a field. Returns a Map. */
1300
+ groupBy(accessor) {
1264
1301
  return new Query(this._config).groupBy(accessor);
1265
1302
  }
1266
- // -------------------------------------------------------------------------
1267
- // Reads — chainable
1268
- // -------------------------------------------------------------------------
1303
+ /** Get all rows as an array. */
1304
+ toArray() {
1305
+ return new Query(this._config);
1306
+ }
1307
+ /** Filter rows by a predicate. Returns a chainable Query. */
1269
1308
  filter(predicate) {
1270
1309
  return new Query(this._config).filter(predicate);
1271
1310
  }
1311
+ /** Sort rows by a field. Returns a chainable Query. */
1272
1312
  sortBy(accessor) {
1273
1313
  return new Query(this._config).sortBy(accessor);
1274
1314
  }
@@ -1295,7 +1335,11 @@ var Table = class {
1295
1335
  this._config.columns
1296
1336
  );
1297
1337
  }
1298
- return void 0;
1338
+ throw new MindStudioError(
1339
+ `Insert into '${this._config.tableName}' succeeded but returned no row. This may indicate a constraint violation.`,
1340
+ "insert_failed",
1341
+ 500
1342
+ );
1299
1343
  });
1300
1344
  const result = isArray ? rows : rows[0];
1301
1345
  this._syncRolesIfNeeded(
@@ -1319,6 +1363,13 @@ var Table = class {
1319
1363
  this._config.columns
1320
1364
  );
1321
1365
  return new Mutation(this._config, [query], (results) => {
1366
+ if (!results[0]?.rows[0]) {
1367
+ throw new MindStudioError(
1368
+ `Row not found: no row with ID '${id}' in table '${this._config.tableName}'`,
1369
+ "row_not_found",
1370
+ 404
1371
+ );
1372
+ }
1322
1373
  const result = deserializeRow(
1323
1374
  results[0].rows[0],
1324
1375
  this._config.columns
@@ -1333,7 +1384,9 @@ var Table = class {
1333
1384
  }
1334
1385
  remove(id) {
1335
1386
  const query = buildDelete(this._config.tableName, `id = ?`, [id]);
1336
- return new Mutation(this._config, [query], () => void 0);
1387
+ return new Mutation(this._config, [query], (results) => ({
1388
+ deleted: results[0].changes > 0
1389
+ }));
1337
1390
  }
1338
1391
  /**
1339
1392
  * Remove all rows matching a predicate. Returns the count removed.
@@ -1367,7 +1420,7 @@ var Table = class {
1367
1420
  }
1368
1421
  clear() {
1369
1422
  const query = buildDelete(this._config.tableName);
1370
- return new Mutation(this._config, [query], () => void 0);
1423
+ return new Mutation(this._config, [query], (results) => results[0].changes);
1371
1424
  }
1372
1425
  /**
1373
1426
  * Insert a row, or update it if a row with the same unique key already
@@ -1385,6 +1438,15 @@ var Table = class {
1385
1438
  this._validateUniqueConstraint(conflictColumns);
1386
1439
  const withDefaults = this._config.defaults ? { ...this._config.defaults, ...data } : data;
1387
1440
  this._checkManagedColumns(withDefaults);
1441
+ for (const col of conflictColumns) {
1442
+ if (!(col in withDefaults)) {
1443
+ throw new MindStudioError(
1444
+ `Upsert on ${this._config.tableName} requires "${col}" in data (conflict key)`,
1445
+ "missing_conflict_key",
1446
+ 400
1447
+ );
1448
+ }
1449
+ }
1388
1450
  const query = buildUpsert(
1389
1451
  this._config.tableName,
1390
1452
  withDefaults,
@@ -1392,6 +1454,13 @@ var Table = class {
1392
1454
  this._config.columns
1393
1455
  );
1394
1456
  return new Mutation(this._config, [query], (results) => {
1457
+ if (!results[0]?.rows[0]) {
1458
+ throw new MindStudioError(
1459
+ `Upsert into ${this._config.tableName} returned no row`,
1460
+ "upsert_failed",
1461
+ 500
1462
+ );
1463
+ }
1395
1464
  const result = deserializeRow(
1396
1465
  results[0].rows[0],
1397
1466
  this._config.columns
@@ -1543,7 +1612,7 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
1543
1612
  if (c.type === "query") {
1544
1613
  if (!c.query && c.predicates?.length) {
1545
1614
  console.warn(
1546
- `[mindstudio] db.batch(): filter on ${c.config.tableName} could not be compiled to SQL \u2014 processing in JS`
1615
+ `[mindstudio] db.batch(): filter on '${c.config.tableName}' could not be compiled to SQL \u2014 processing in JS`
1547
1616
  );
1548
1617
  }
1549
1618
  return Query._processResults(results[0], c);
@@ -3018,9 +3087,14 @@ var stepMetadata = {
3018
3087
  },
3019
3088
  "sendEmail": {
3020
3089
  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"] },
3090
+ description: "Send an email to one or more recipient addresses.",
3091
+ 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.
3092
+ - 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.
3093
+ - If the body is a URL to a hosted HTML file on the CDN, the HTML is fetched and used as the email body.
3094
+ - When generateHtml is enabled, the body text is converted to a styled HTML email using an AI model.
3095
+ - connectionId can be a comma-separated list to send to multiple recipients.
3096
+ - The special connectionId "trigger_email" uses the email address that triggered the workflow.`,
3097
+ 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
3098
  outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "Email addresses the message was sent to" } }, "required": ["recipients"] }
3025
3099
  },
3026
3100
  "sendGmailDraft": {
@@ -3342,7 +3416,7 @@ var MindStudioAgent = class {
3342
3416
  const res = await fetch(data.outputUrl);
3343
3417
  if (!res.ok) {
3344
3418
  throw new MindStudioError(
3345
- `Failed to fetch output from S3: ${res.status} ${res.statusText}`,
3419
+ `Failed to fetch ${stepType} output from S3: ${res.status} ${res.statusText}`,
3346
3420
  "output_fetch_error",
3347
3421
  res.status
3348
3422
  );
@@ -3405,13 +3479,27 @@ var MindStudioAgent = class {
3405
3479
  this._httpConfig.rateLimiter.updateFromHeaders(res.headers);
3406
3480
  if (!res.ok) {
3407
3481
  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
- );
3482
+ let message = `${res.status} ${res.statusText}`;
3483
+ let code = "api_error";
3484
+ let details;
3485
+ try {
3486
+ const text = await res.text();
3487
+ try {
3488
+ const body2 = JSON.parse(text);
3489
+ details = body2;
3490
+ 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);
3491
+ if (errMsg) message = errMsg;
3492
+ else if (body2.error || body2.message || body2.details) {
3493
+ message = JSON.stringify(body2.error ?? body2.message ?? body2.details);
3494
+ }
3495
+ if (body2.code) code = body2.code;
3496
+ } catch {
3497
+ const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
3498
+ if (stripped) message = stripped.slice(0, 200);
3499
+ }
3500
+ } catch {
3501
+ }
3502
+ throw new MindStudioError(`[${stepType}] ${message}`, code, res.status, details);
3415
3503
  }
3416
3504
  const headers = res.headers;
3417
3505
  try {
@@ -3444,7 +3532,7 @@ var MindStudioAgent = class {
3444
3532
  };
3445
3533
  } else if (event.type === "error") {
3446
3534
  throw new MindStudioError(
3447
- event.error || "Step execution failed",
3535
+ `[${stepType}] ${event.error || "Step execution failed"}`,
3448
3536
  "step_error",
3449
3537
  500
3450
3538
  );
@@ -3483,7 +3571,7 @@ var MindStudioAgent = class {
3483
3571
  }
3484
3572
  if (!doneEvent) {
3485
3573
  throw new MindStudioError(
3486
- "Stream ended without a done event",
3574
+ `[${stepType}] Stream ended unexpectedly without completing. The step execution may have been interrupted.`,
3487
3575
  "stream_error",
3488
3576
  500
3489
3577
  );
@@ -3495,7 +3583,7 @@ var MindStudioAgent = class {
3495
3583
  const s3Res = await fetch(doneEvent.outputUrl);
3496
3584
  if (!s3Res.ok) {
3497
3585
  throw new MindStudioError(
3498
- `Failed to fetch output from S3: ${s3Res.status} ${s3Res.statusText}`,
3586
+ `Failed to fetch ${stepType} output from S3: ${s3Res.status} ${s3Res.statusText}`,
3499
3587
  "output_fetch_error",
3500
3588
  s3Res.status
3501
3589
  );
@@ -3856,7 +3944,7 @@ var MindStudioAgent = class {
3856
3944
  }
3857
3945
  if (!this._auth) {
3858
3946
  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.",
3947
+ "Auth context not loaded. Call `await agent.ensureContext()` first, or perform any db operation (which auto-loads context).",
3860
3948
  "context_not_loaded",
3861
3949
  400
3862
3950
  );
@@ -4030,13 +4118,12 @@ var MindStudioAgent = class {
4030
4118
  if (!res.ok) {
4031
4119
  const text = await res.text().catch(() => "");
4032
4120
  console.warn(
4033
- `[mindstudio] Failed to sync roles for user ${userId}: ${res.status} ${text}`
4121
+ `[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
4122
  );
4035
4123
  }
4036
4124
  } catch (err) {
4037
4125
  console.warn(
4038
- `[mindstudio] Failed to sync roles for user ${userId}:`,
4039
- err
4126
+ `[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
4127
  );
4041
4128
  }
4042
4129
  }