@promptev/context-engine 0.0.3 → 0.0.5

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
@@ -5779,15 +5779,16 @@ function parseDocumentId(documentId) {
5779
5779
  }
5780
5780
  return raw;
5781
5781
  }
5782
- function scopeSql(sourceIds, principals, params) {
5782
+ function scopeSql(sourceIds, principals, params, alias = "") {
5783
+ const col = alias ? `${alias}.` : "";
5783
5784
  const where = [];
5784
5785
  if (sourceIds != null) {
5785
5786
  params.push(sourceIds);
5786
- where.push(`source_id = ANY($${params.length}::text[])`);
5787
+ where.push(`${col}source_id = ANY($${params.length}::text[])`);
5787
5788
  }
5788
5789
  if (principals != null) {
5789
5790
  params.push(principals);
5790
- where.push(`(acl IS NULL OR acl && $${params.length}::text[])`);
5791
+ where.push(`(${col}acl IS NULL OR ${col}acl && $${params.length}::text[])`);
5791
5792
  }
5792
5793
  return where.length ? `WHERE ${where.join(" AND ")}` : "";
5793
5794
  }
@@ -6046,6 +6047,19 @@ async function listDocuments(opts) {
6046
6047
  const principals = opts.principals ?? null;
6047
6048
  const params = [];
6048
6049
  let where = scopeSql(sourceIds, principals, params);
6050
+ if (opts.documentIds != null) {
6051
+ const parsedIds = [];
6052
+ for (const did of opts.documentIds) {
6053
+ try {
6054
+ parsedIds.push(parseDocumentId(did));
6055
+ } catch {
6056
+ console.warn("listDocuments: skipping invalid document id %s", did);
6057
+ }
6058
+ }
6059
+ params.push(parsedIds);
6060
+ const clause = `id = ANY($${params.length}::uuid[])`;
6061
+ where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
6062
+ }
6049
6063
  const cursorTime = parsedCursor?.time ?? parsedCursor?.created_at;
6050
6064
  const cursorId = parsedCursor?.id;
6051
6065
  if (cursorTime && cursorId && UUID_RE2.test(String(cursorId))) {
@@ -6105,6 +6119,350 @@ async function listDocuments(opts) {
6105
6119
  }
6106
6120
  return result;
6107
6121
  }
6122
+ async function spreadsheetSchema(opts) {
6123
+ const ids = [];
6124
+ for (const did of opts.documentIds ?? []) {
6125
+ try {
6126
+ ids.push(parseDocumentId(did));
6127
+ } catch {
6128
+ console.warn("spreadsheetSchema: skipping invalid document id %s", did);
6129
+ }
6130
+ }
6131
+ if (!ids.length) return [];
6132
+ const principals = opts.principals ?? null;
6133
+ const budget = opts.budget ?? MAX_SCHEMA_TEXT_CHARS;
6134
+ const params = [];
6135
+ let where = scopeSql(opts.sourceIds ?? null, principals, params);
6136
+ const mimeClause = TABULAR_MIME_PATTERNS.map((pattern) => {
6137
+ params.push(pattern);
6138
+ return `mime_type ILIKE $${params.length}`;
6139
+ }).join(" OR ");
6140
+ where = where ? `${where} AND (${mimeClause})` : `WHERE (${mimeClause})`;
6141
+ params.push(ids);
6142
+ where += ` AND id = ANY($${params.length}::uuid[])`;
6143
+ const sized = await opts.pool.query(
6144
+ `SELECT id, name, source_id, mime_type, COALESCE(LENGTH(text), 0) AS text_len
6145
+ FROM context_engine_documents
6146
+ ${where}`,
6147
+ params
6148
+ );
6149
+ const byId = /* @__PURE__ */ new Map();
6150
+ for (const row of sized.rows) {
6151
+ if (isTabularMime(row.mime_type)) byId.set(String(row.id), row);
6152
+ }
6153
+ const rows = ids.map((id) => byId.get(id)).filter((r) => r != null);
6154
+ const chosen = [];
6155
+ let spent = 0;
6156
+ for (const row of rows) {
6157
+ const len = Number(row.text_len ?? 0);
6158
+ if (chosen.length && spent + len > budget) break;
6159
+ chosen.push(String(row.id));
6160
+ spent += len;
6161
+ }
6162
+ const texts = /* @__PURE__ */ new Map();
6163
+ if (chosen.length) {
6164
+ const bodyParams = [];
6165
+ const bodyWhere = scopeSql(opts.sourceIds ?? null, principals, bodyParams);
6166
+ bodyParams.push(chosen);
6167
+ const clause = `id = ANY($${bodyParams.length}::uuid[])`;
6168
+ const { rows: bodies } = await opts.pool.query(
6169
+ `SELECT id, text FROM context_engine_documents
6170
+ ${bodyWhere ? `${bodyWhere} AND ${clause}` : `WHERE ${clause}`}`,
6171
+ bodyParams
6172
+ );
6173
+ for (const row of bodies) texts.set(String(row.id), row.text ?? null);
6174
+ }
6175
+ return rows.map((row) => {
6176
+ const id = String(row.id);
6177
+ if (!texts.has(id)) {
6178
+ return {
6179
+ document_id: id,
6180
+ name: row.name,
6181
+ source_id: row.source_id,
6182
+ sheets: null,
6183
+ schema_unavailable: "not read: this page of spreadsheets is past the text budget \u2014 narrow with sourceIds, or ask for a smaller limit"
6184
+ };
6185
+ }
6186
+ return {
6187
+ document_id: id,
6188
+ name: row.name,
6189
+ source_id: row.source_id,
6190
+ sheets: redactValueRecursive(spreadsheetSchemaFromText(texts.get(id)), opts.redaction, {
6191
+ principals,
6192
+ secretKey: opts.secretKey ?? null,
6193
+ hooks: opts.hooks
6194
+ })
6195
+ };
6196
+ });
6197
+ }
6198
+ function remainder(structure) {
6199
+ let total = 0;
6200
+ for (const [key, value] of Object.entries(structure)) {
6201
+ if (key.startsWith("more_") && typeof value === "number") total += value;
6202
+ }
6203
+ const sheets = structure.sheets;
6204
+ if (Array.isArray(sheets)) {
6205
+ for (const sheet of sheets) {
6206
+ if (sheet && typeof sheet === "object" && typeof sheet.more_columns === "number") {
6207
+ total += sheet.more_columns;
6208
+ }
6209
+ }
6210
+ }
6211
+ return total;
6212
+ }
6213
+ function boundedSheets(sheets) {
6214
+ if (!sheets?.length) return sheets;
6215
+ return sheets.slice(0, MAX_STRUCTURE_ITEMS).map((sheet) => {
6216
+ const columns = sheet.columns ?? [];
6217
+ const trimmed = {
6218
+ ...sheet,
6219
+ columns: columns.slice(0, MAX_STRUCTURE_ITEMS)
6220
+ };
6221
+ if (columns.length > MAX_STRUCTURE_ITEMS) {
6222
+ trimmed.more_columns = columns.length - MAX_STRUCTURE_ITEMS;
6223
+ }
6224
+ return trimmed;
6225
+ });
6226
+ }
6227
+ async function documentStructure(opts) {
6228
+ const ids = [];
6229
+ for (const did of opts.documentIds ?? []) {
6230
+ try {
6231
+ ids.push(parseDocumentId(did));
6232
+ } catch {
6233
+ console.warn("documentStructure: skipping invalid document id %s", did);
6234
+ }
6235
+ }
6236
+ if (!ids.length) return {};
6237
+ const principals = opts.principals ?? null;
6238
+ const bounded = opts.bounded ?? true;
6239
+ const described2 = await spreadsheetSchema({
6240
+ pool: opts.pool,
6241
+ documentIds: ids,
6242
+ sourceIds: opts.sourceIds,
6243
+ principals,
6244
+ budget: opts.budget,
6245
+ redaction: opts.redaction,
6246
+ secretKey: opts.secretKey,
6247
+ hooks: opts.hooks
6248
+ });
6249
+ const tabular = new Map(described2.map((entry) => [entry.document_id, entry]));
6250
+ const params = [];
6251
+ let where = scopeSql(opts.sourceIds ?? null, principals, params, "d");
6252
+ params.push(ids);
6253
+ const clause = `c.document_id = ANY($${params.length}::uuid[])`;
6254
+ where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
6255
+ const { rows } = await opts.pool.query(
6256
+ `SELECT c.document_id AS document_id,
6257
+ c.meta_data->>'section_title' AS section,
6258
+ c.meta_data->>'top_level_key' AS key,
6259
+ MIN(c.idx) AS first_idx,
6260
+ COUNT(*) AS chunks,
6261
+ -- Only the page-marker pass writes the page number, and it writes an int
6262
+ -- \u2014 but a cast that meets anything else raises for the whole
6263
+ -- query, so the type is checked in SQL rather than assumed.
6264
+ MAX(CASE WHEN jsonb_typeof(c.meta_data->'page') = 'number'
6265
+ THEN (c.meta_data->>'page')::int END) AS page
6266
+ FROM context_engine_chunks c
6267
+ JOIN context_engine_documents d ON d.id = c.document_id
6268
+ ${where}
6269
+ GROUP BY c.document_id, section, key`,
6270
+ params
6271
+ );
6272
+ const outline = /* @__PURE__ */ new Map();
6273
+ for (const row of rows) {
6274
+ const id = String(row.document_id);
6275
+ let found = outline.get(id);
6276
+ if (!found) {
6277
+ found = { chunks: 0, page: null, parts: [] };
6278
+ outline.set(id, found);
6279
+ }
6280
+ found.chunks += Number(row.chunks ?? 0);
6281
+ if (row.page != null) found.page = Math.max(found.page ?? 0, Number(row.page));
6282
+ found.parts.push([Number(row.first_idx ?? 0), row.section ?? null, row.key ?? null]);
6283
+ }
6284
+ const ordered = (parts, pick, cap) => {
6285
+ const seen = [];
6286
+ for (const part of [...parts].sort((a, b) => a[0] - b[0])) {
6287
+ const value = pick(part);
6288
+ if (value && !seen.includes(value)) seen.push(value);
6289
+ }
6290
+ return [cap === null ? seen : seen.slice(0, cap), seen.length];
6291
+ };
6292
+ const out = {};
6293
+ for (const id of ids) {
6294
+ const found = outline.get(id);
6295
+ const structure = { chunks: found ? found.chunks : 0 };
6296
+ const sheetDoc = tabular.get(id);
6297
+ if (sheetDoc) {
6298
+ const sheets = sheetDoc.sheets ?? null;
6299
+ structure.sheets = bounded ? boundedSheets(sheets) : sheets;
6300
+ if (bounded && sheets && sheets.length > MAX_STRUCTURE_ITEMS) {
6301
+ structure.more_sheets = sheets.length - MAX_STRUCTURE_ITEMS;
6302
+ }
6303
+ if (sheetDoc.schema_unavailable) structure.schema_unavailable = sheetDoc.schema_unavailable;
6304
+ out[id] = structure;
6305
+ continue;
6306
+ }
6307
+ if (found) {
6308
+ if (found.page != null) {
6309
+ structure.last_page = found.page;
6310
+ }
6311
+ const cap = bounded ? MAX_STRUCTURE_ITEMS : null;
6312
+ const [sections, totalSections] = ordered(found.parts, (p) => p[1], cap);
6313
+ if (sections.length) {
6314
+ structure.sections = sections;
6315
+ if (totalSections > sections.length) structure.more_sections = totalSections - sections.length;
6316
+ }
6317
+ const [keys, totalKeys] = ordered(found.parts, (p) => p[2], cap);
6318
+ if (keys.length) {
6319
+ structure.keys = keys;
6320
+ if (totalKeys > keys.length) structure.more_keys = totalKeys - keys.length;
6321
+ }
6322
+ }
6323
+ out[id] = structure;
6324
+ }
6325
+ for (const [id, structure] of Object.entries(out)) {
6326
+ const left = remainder(structure);
6327
+ if (!left) continue;
6328
+ if (left <= MAX_INVITED_ITEMS) {
6329
+ structure.next_action = { action: "discover", document_ids: [id] };
6330
+ } else if ("sheets" in structure) {
6331
+ structure.instead = "too many to list: a sheet this wide is one to COMPUTE over, not to read. Use action 'compute' and name the columns above, or search for a value to find the one row you want.";
6332
+ } else {
6333
+ structure.instead = "too many to list: read the document with get_chunks, or search it for the part you need.";
6334
+ }
6335
+ }
6336
+ const redactOpts = { principals, secretKey: opts.secretKey ?? null, hooks: opts.hooks };
6337
+ return Object.fromEntries(
6338
+ Object.entries(out).map(([id, structure]) => [
6339
+ id,
6340
+ redactValueRecursive(structure, opts.redaction, redactOpts)
6341
+ ])
6342
+ );
6343
+ }
6344
+ async function documentTypes(opts) {
6345
+ const principals = opts.principals ?? null;
6346
+ const params = [];
6347
+ let where = scopeSql(opts.sourceIds ?? null, principals, params);
6348
+ if (opts.documentIds != null) {
6349
+ const parsedIds = [];
6350
+ for (const did of opts.documentIds) {
6351
+ try {
6352
+ parsedIds.push(parseDocumentId(did));
6353
+ } catch {
6354
+ console.warn("documentTypes: skipping invalid document id %s", did);
6355
+ }
6356
+ }
6357
+ params.push(parsedIds);
6358
+ const clause = `id = ANY($${params.length}::uuid[])`;
6359
+ where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
6360
+ }
6361
+ params.push(MAX_CENSUS_ROWS);
6362
+ const { rows } = await opts.pool.query(
6363
+ `SELECT document_type AS doc_type,
6364
+ mime_type,
6365
+ COUNT(*) AS documents,
6366
+ COUNT(*) FILTER (
6367
+ WHERE structured_data IS NOT NULL AND structured_data::text <> '{}'
6368
+ ) AS with_fields
6369
+ FROM context_engine_documents
6370
+ ${where}
6371
+ GROUP BY document_type, mime_type
6372
+ ORDER BY COUNT(*) DESC
6373
+ LIMIT $${params.length}`,
6374
+ params
6375
+ );
6376
+ const buckets = /* @__PURE__ */ new Map();
6377
+ for (const row of rows) {
6378
+ const kind = isTabularMime(row.mime_type) ? "spreadsheet" : "text";
6379
+ const type = row.doc_type ?? null;
6380
+ const bucketKey = `${kind}\0${type ?? ""}`;
6381
+ let bucket = buckets.get(bucketKey);
6382
+ if (!bucket) {
6383
+ bucket = { kind, type, documents: 0, with_fields: 0 };
6384
+ buckets.set(bucketKey, bucket);
6385
+ }
6386
+ bucket.documents += Number(row.documents ?? 0);
6387
+ bucket.with_fields += Number(row.with_fields ?? 0);
6388
+ }
6389
+ const census = [...buckets.values()].sort(
6390
+ (a, b) => b.documents - a.documents || a.kind.localeCompare(b.kind) || (a.type ?? "").localeCompare(b.type ?? "")
6391
+ ).slice(0, opts.limit ?? MAX_DOCUMENT_TYPES);
6392
+ return redactValueRecursive(census, opts.redaction, {
6393
+ principals,
6394
+ secretKey: opts.secretKey ?? null,
6395
+ hooks: opts.hooks
6396
+ });
6397
+ }
6398
+ async function fieldSummary(opts) {
6399
+ const principals = opts.principals ?? null;
6400
+ const maxFieldsPerType = opts.maxFieldsPerType ?? MAX_FIELDS_PER_TYPE;
6401
+ const params = [];
6402
+ let where = scopeSql(opts.sourceIds ?? null, principals, params);
6403
+ if (opts.documentIds != null) {
6404
+ const parsedIds = [];
6405
+ for (const did of opts.documentIds) {
6406
+ try {
6407
+ parsedIds.push(parseDocumentId(did));
6408
+ } catch {
6409
+ console.warn("fieldSummary: skipping invalid document id %s", did);
6410
+ }
6411
+ }
6412
+ params.push(parsedIds);
6413
+ const clause = `id = ANY($${params.length}::uuid[])`;
6414
+ where = where ? `${where} AND ${clause}` : `WHERE ${clause}`;
6415
+ }
6416
+ const typed = "jsonb_typeof(structured_data) = 'object'";
6417
+ where = where ? `${where} AND ${typed}` : `WHERE ${typed}`;
6418
+ params.push(MAX_FIELD_ROWS);
6419
+ const { rows } = await opts.pool.query(
6420
+ `SELECT d.document_type AS doc_type,
6421
+ d.mime_type AS mime_type,
6422
+ kv.key AS key,
6423
+ COUNT(*) AS documents,
6424
+ MAX(kv.value::text) AS sample
6425
+ FROM context_engine_documents d
6426
+ JOIN LATERAL jsonb_each(d.structured_data) AS kv(key, value) ON TRUE
6427
+ ${where}
6428
+ GROUP BY d.document_type, d.mime_type, kv.key
6429
+ ORDER BY d.document_type, COUNT(*) DESC, kv.key
6430
+ LIMIT $${params.length}`,
6431
+ params
6432
+ );
6433
+ const groups = /* @__PURE__ */ new Map();
6434
+ for (const row of rows) {
6435
+ const kind = isTabularMime(row.mime_type) ? "spreadsheet" : "text";
6436
+ const type = row.doc_type ?? null;
6437
+ const groupKey = `${kind}\0${type ?? ""}`;
6438
+ let group = groups.get(groupKey);
6439
+ if (!group) {
6440
+ group = { kind, type, fields: [] };
6441
+ groups.set(groupKey, group);
6442
+ }
6443
+ let sample = null;
6444
+ try {
6445
+ sample = row.sample == null ? null : JSON.parse(String(row.sample));
6446
+ } catch {
6447
+ sample = row.sample;
6448
+ }
6449
+ if (group.fields.length < maxFieldsPerType) {
6450
+ group.fields.push({
6451
+ field: String(row.key),
6452
+ type: inferDataType(String(row.key), sample),
6453
+ documents: Number(row.documents)
6454
+ });
6455
+ } else {
6456
+ group.more_fields ??= [];
6457
+ group.more_fields.push(String(row.key));
6458
+ }
6459
+ }
6460
+ return redactValueRecursive([...groups.values()], opts.redaction, {
6461
+ principals,
6462
+ secretKey: opts.secretKey ?? null,
6463
+ hooks: opts.hooks
6464
+ });
6465
+ }
6108
6466
  function candidateFieldTokens(question) {
6109
6467
  const candidates = [];
6110
6468
  const seen = /* @__PURE__ */ new Set();
@@ -6185,8 +6543,7 @@ async function requireDanfo() {
6185
6543
  throw new ExtraMissingError("compute", "danfojs-node", "compute() dataframes");
6186
6544
  }
6187
6545
  }
6188
- function parseSpreadsheetText(dfd, text) {
6189
- if (!text) return {};
6546
+ function splitSheets(text) {
6190
6547
  const sheets = {};
6191
6548
  let current = "sheet1";
6192
6549
  for (const line of text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n")) {
@@ -6203,6 +6560,34 @@ function parseSpreadsheetText(dfd, text) {
6203
6560
  }
6204
6561
  bucket.push(line);
6205
6562
  }
6563
+ return sheets;
6564
+ }
6565
+ function spreadsheetSchemaFromText(text) {
6566
+ if (!text) return [];
6567
+ const out = [];
6568
+ for (const [name, lines] of Object.entries(splitSheets(text))) {
6569
+ const body = lines.join("\n").trim();
6570
+ if (!body) continue;
6571
+ let rows;
6572
+ try {
6573
+ rows = parseCsv(body);
6574
+ } catch (exc) {
6575
+ console.warn("discover: sheet '%s' not parseable as CSV: %s", name, exc);
6576
+ continue;
6577
+ }
6578
+ rows = rows.filter((row) => row.some((cell) => (cell ?? "").trim()));
6579
+ if (!rows.length) continue;
6580
+ out.push({
6581
+ name,
6582
+ columns: rows[0].map((cell) => String(cell).trim()),
6583
+ row_count: rows.length - 1
6584
+ });
6585
+ }
6586
+ return out;
6587
+ }
6588
+ function parseSpreadsheetText(dfd, text) {
6589
+ if (!text) return {};
6590
+ const sheets = splitSheets(text);
6206
6591
  const out = {};
6207
6592
  for (const [name, lines] of Object.entries(sheets)) {
6208
6593
  const body = lines.join("\n").trim();
@@ -6415,7 +6800,7 @@ ${schemaLines.join("\n")}`;
6415
6800
  hooks
6416
6801
  });
6417
6802
  }
6418
- var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, TABULAR_MIME_PATTERNS, UUID_RE2, WORD_RE, STOPWORDS, SHEET_MARKER_RE2, CODE_FENCE_RE, COMPUTE_SYSTEM_PROMPT, visible, MAX_CHUNKS_PER_READ, MAX_MAP_REDUCE_DOCS, DEFAULT_MAP_REDUCE_DOCS, MAX_MAP_REDUCE_CONCURRENCY;
6803
+ var MAX_LIST_LIMIT, MAX_COMPUTE_TEXT_CHARS, DEFAULT_COMPUTE_TIMEOUT, MAX_COMPUTE_DOCUMENTS, MAX_SCHEMA_TEXT_CHARS, MAX_FIELDS_PER_TYPE, MAX_FIELD_ROWS, MAX_STRUCTURE_ITEMS, MAX_INVITED_ITEMS, MAX_DOCUMENT_TYPES, MAX_CENSUS_ROWS, TABULAR_MIME_PATTERNS, UUID_RE2, WORD_RE, STOPWORDS, SHEET_MARKER_RE2, CODE_FENCE_RE, COMPUTE_SYSTEM_PROMPT, visible, MAX_CHUNKS_PER_READ, MAX_MAP_REDUCE_DOCS, DEFAULT_MAP_REDUCE_DOCS, MAX_MAP_REDUCE_CONCURRENCY;
6419
6804
  var init_actions = __esm({
6420
6805
  "src/actions.ts"() {
6421
6806
  init_chunkers();
@@ -6431,6 +6816,13 @@ var init_actions = __esm({
6431
6816
  MAX_COMPUTE_TEXT_CHARS = 2e6;
6432
6817
  DEFAULT_COMPUTE_TIMEOUT = 30;
6433
6818
  MAX_COMPUTE_DOCUMENTS = 50;
6819
+ MAX_SCHEMA_TEXT_CHARS = MAX_COMPUTE_TEXT_CHARS;
6820
+ MAX_FIELDS_PER_TYPE = 40;
6821
+ MAX_FIELD_ROWS = 2e3;
6822
+ MAX_STRUCTURE_ITEMS = 40;
6823
+ MAX_INVITED_ITEMS = 400;
6824
+ MAX_DOCUMENT_TYPES = 30;
6825
+ MAX_CENSUS_ROWS = 500;
6434
6826
  TABULAR_MIME_PATTERNS = ["%csv%", "%sheet%", "%excel%", "%spreadsheetml%", "%tab-separated%"];
6435
6827
  UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6436
6828
  WORD_RE = /[^\W\d_]+/gu;
@@ -8264,11 +8656,16 @@ function parseCursor2(cursor) {
8264
8656
  }
8265
8657
  return parsed;
8266
8658
  }
8267
- async function listPage(engine, sourceIds, principals, action, limit, cursor, ceiling, redaction) {
8659
+ async function listPage(engine, sourceIds, principals, action, limit, cursor, documentIds, ceiling, redaction) {
8268
8660
  const page = await engine.listDocuments({
8269
8661
  // `!= null`, NOT truthiness: an EMPTY array means "nothing is in scope"
8270
8662
  // and collapsing it to null would list the whole corpus.
8271
8663
  sourceIds: sourceIds != null ? [...new Set(sourceIds)] : null,
8664
+ // Already intersected with the host's ceiling by `narrowToCeiling`, and
8665
+ // filtered in SQL rather than after the page is built — three named
8666
+ // documents sitting on page four must come back as themselves, not as an
8667
+ // empty page.
8668
+ documentIds: documentIds != null ? [...new Set(documentIds)] : null,
8272
8669
  principals,
8273
8670
  cursor,
8274
8671
  limit: Math.max(1, Math.min(limit, MAX_LIST_LIMIT)),
@@ -8278,7 +8675,9 @@ async function listPage(engine, sourceIds, principals, action, limit, cursor, ce
8278
8675
  id: String(raw.id),
8279
8676
  name: raw.name,
8280
8677
  source_id: raw.sourceId ?? raw.source_id,
8281
- kind: documentKind(raw)
8678
+ kind: documentKind(raw),
8679
+ document_type: raw.documentType ?? raw.document_type ?? null,
8680
+ mode: raw.mode ?? null
8282
8681
  }));
8283
8682
  if (ceiling.documentIds != null) {
8284
8683
  const allowed = new Set(ceiling.documentIds);
@@ -8331,30 +8730,62 @@ async function callKnowledgeTool(engine, args) {
8331
8730
  action,
8332
8731
  args.limit ?? 50,
8333
8732
  parseCursor2(args.cursor),
8733
+ documentIds,
8334
8734
  ceiling,
8335
8735
  args.redaction
8336
8736
  );
8337
8737
  if (action === "list") return { success: true, ...page };
8338
- const spreadsheets = page.documents.filter((d) => d.kind === "spreadsheet").map((d) => d.name);
8738
+ const structures = await engine.documentStructure({
8739
+ documentIds: page.documents.map((d) => d.id),
8740
+ sourceIds,
8741
+ principals,
8742
+ // The cap is for the call that did NOT name its documents. A caller
8743
+ // that asked about specific documents asked for all of them, and the
8744
+ // truncated payload tells it to make exactly this call — so answering
8745
+ // it truncated again would be a loop.
8746
+ bounded: !requestedDocumentIds?.length,
8747
+ redaction: args.redaction
8748
+ });
8749
+ for (const doc of page.documents) doc.structure = structures[doc.id] ?? {};
8750
+ const hasSpreadsheet = page.documents.some((d) => d.kind === "spreadsheet");
8751
+ const fieldsByType = await engine.fieldSummary({
8752
+ sourceIds,
8753
+ documentIds,
8754
+ principals,
8755
+ redaction: args.redaction
8756
+ });
8757
+ const census = await engine.documentTypes({
8758
+ sourceIds,
8759
+ documentIds,
8760
+ principals,
8761
+ redaction: args.redaction
8762
+ });
8339
8763
  const forAFact = { action: "search", query: "<bare identifier or key words>" };
8340
8764
  const nextAction = {};
8341
- if (spreadsheets.length && available.compute) {
8765
+ if (hasSpreadsheet && available.compute) {
8342
8766
  nextAction["for a figure from a spreadsheet"] = {
8343
8767
  action: "compute",
8344
- query: "<what to compute, columns as named>"
8768
+ query: "<what to compute, columns as named above>"
8345
8769
  };
8346
8770
  }
8347
8771
  nextAction["for a clause or a fact"] = forAFact;
8772
+ if (fieldsByType.length && available.query_meta) {
8773
+ nextAction["for documents by a field value"] = {
8774
+ action: "query_meta",
8775
+ query: "<a question naming a field from fields_by_type>"
8776
+ };
8777
+ }
8348
8778
  if (available.get_neighbors) {
8349
8779
  nextAction["for how things connect"] = {
8350
8780
  action: "get_neighbors",
8351
8781
  entity: "<a name that appears in the documents>"
8352
8782
  };
8353
8783
  }
8354
- return {
8784
+ const discovered = {
8355
8785
  success: true,
8356
8786
  ...page,
8357
- spreadsheets,
8787
+ document_types: census,
8788
+ fields_by_type: fieldsByType,
8358
8789
  available_actions: Object.fromEntries(
8359
8790
  Object.keys(ACTION_HELP).map((name) => [
8360
8791
  name,
@@ -8363,6 +8794,7 @@ async function callKnowledgeTool(engine, args) {
8363
8794
  ),
8364
8795
  next_action: nextAction
8365
8796
  };
8797
+ return discovered;
8366
8798
  }
8367
8799
  if (action === "search") {
8368
8800
  if (!text) return { success: false, error: "search needs a query" };
@@ -8592,7 +9024,7 @@ var init_knowledge_tool = __esm({
8592
9024
  "get_neighbors",
8593
9025
  "community_summary"
8594
9026
  ];
8595
- KNOWLEDGE_TOOL_DESCRIPTION = "The knowledge base \u2014 the ingested documents and data files (PDF, Word, Excel, CSV and the rest) \u2014 as ONE tool with actions. Nothing is searched for you: use it before answering anything that should come from those documents, and do not use it for general knowledge. Call action 'discover' FIRST when you do not already know what is there: it lists the documents, says which are spreadsheets, and says which actions this deployment can run. Then match the action to the task. 'search' finds passages by meaning or keywords \u2014 for questions answered by reading text. Looking up an identifier (an ID, code, SKU or invoice number) is the exception: search the BARE identifier alone, e.g. '2525', never the whole question. Those are CONTENT identifiers \u2014 written inside a document \u2014 and they belong in a query; a document_id is a system id (a uuid) that only discover, list or a search hit can give you, so never search a uuid as text and never hand an invoice number to get_doc. 'get_doc' reads one whole document by id, 'get_docs' reads several at once, and 'get_chunks' walks one long document in order a piece at a time when you need more of it than an excerpt; 'list' browses the documents without searching. 'map_reduce' asks the SAME question of every document in scope and answers once per document \u2014 for 'which contracts mention X', where search would return a handful of passages and miss the rest. 'query_meta' filters and aggregates documents by their structured fields (dates, amounts, categories) \u2014 usually the right action for a question about spreadsheet data. 'compute' runs code over the spreadsheets for any figure DERIVED from them \u2014 a total, average, count, ranking, margin or comparison across rows \u2014 AND for finding the exact row matching one id or value. In a large table, search cannot reliably locate an individual row; compute can. When the question is about how things are CONNECTED rather than what a document says \u2014 who works with whom, what belongs to what, what a change touches \u2014 use the graph actions: 'get_neighbors' for what is one step from one thing, 'traverse' for everything within a few steps of it, 'find_related' for connections of a kind across the corpus, and 'community_summary' for the themes the corpus groups into. They are available only where a graph was built; discover says so. Searching with mode='graph' ranks passages by those same connections instead of by wording alone, which finds a passage that never repeats your words. Rules that decide answers: search returns EXCERPTS, and rows of a spreadsheet are not arithmetic \u2014 never add up, average or rank rows yourself from what search returned, and never say a figure is not available before computing over the sheet that holds it. A listing that says has_more has MORE: send its next_cursor back as 'cursor' for the next page, and never conclude a document is absent from a first page that was truncated. Search again with different words before saying a document is missing. A result may carry a next_action (or next_page): it is the call to make next, already filled in \u2014 follow it rather than guessing the next step. Cite document names.";
9027
+ KNOWLEDGE_TOOL_DESCRIPTION = "The knowledge base \u2014 the ingested documents and data files (PDF, Word, Excel, CSV and the rest) \u2014 as ONE tool with actions. Nothing is searched for you: use it before answering anything that should come from those documents, and do not use it for general knowledge. Call action 'discover' FIRST when you do not already know what is there: it lists the documents and says what is INSIDE each one \u2014 a spreadsheet's sheet names, column headers and row counts; a document's section titles and last page; a JSON file's top-level keys \u2014 plus the extracted field names grouped by document type with each field's data type and how many documents carry it, a census of the whole corpus, and which actions this deployment can run. Use those names verbatim: they are what compute, query_meta and get_chunks match on, so one discover is enough and you never have to go looking for a column, a section or a field name. A long list is shortened there, with the rest reported as a count and, when it is worth the trip, the exact call that returns it in full \u2014 discover again with document_ids set to that one document, which answers whole. When it is not worth the trip the payload says so and says what to do instead: a sheet with hundreds of columns is one to compute over, never one to read back. Then match the action to the task. 'search' finds passages by meaning or keywords \u2014 for questions answered by reading text. Looking up an identifier (an ID, code, SKU or invoice number) is the exception: search the BARE identifier alone, e.g. '2525', never the whole question. Those are CONTENT identifiers \u2014 written inside a document \u2014 and they belong in a query; a document_id is a system id (a uuid) that only discover, list or a search hit can give you, so never search a uuid as text and never hand an invoice number to get_doc. 'get_doc' reads one whole document by id, 'get_docs' reads several at once, and 'get_chunks' walks one long document in order a piece at a time when you need more of it than an excerpt; 'list' browses the documents without searching. 'map_reduce' asks the SAME question of every document in scope and answers once per document \u2014 for 'which contracts mention X', where search would return a handful of passages and miss the rest. 'query_meta' filters and aggregates documents by their structured fields (dates, amounts, categories) \u2014 usually the right action for a question about spreadsheet data. 'compute' runs code over the spreadsheets for any figure DERIVED from them \u2014 a total, average, count, ranking, margin or comparison across rows \u2014 AND for finding the exact row matching one id or value. In a large table, search cannot reliably locate an individual row; compute can. When the question is about how things are CONNECTED rather than what a document says \u2014 who works with whom, what belongs to what, what a change touches \u2014 use the graph actions: 'get_neighbors' for what is one step from one thing, 'traverse' for everything within a few steps of it, 'find_related' for connections of a kind across the corpus, and 'community_summary' for the themes the corpus groups into. They are available only where a graph was built; discover says so. Searching with mode='graph' ranks passages by those same connections instead of by wording alone, which finds a passage that never repeats your words. Rules that decide answers: search returns EXCERPTS, and rows of a spreadsheet are not arithmetic \u2014 never add up, average or rank rows yourself from what search returned, and never say a figure is not available before computing over the sheet that holds it. A listing that says has_more has MORE: send its next_cursor back as 'cursor' for the next page, and never conclude a document is absent from a first page that was truncated. Search again with different words before saying a document is missing. A result may carry a next_action (or next_page): it is the call to make next, already filled in \u2014 follow it rather than guessing the next step. Cite document names.";
8596
9028
  ACTION_PARAM_DESCRIPTION = "Match it to the task: reading questions -> search (a bare identifier for an ID or code); spreadsheet analysis, or the exact row for one id or value -> query_meta or compute; how things connect -> get_neighbors, traverse, find_related or community_summary; browse everything -> list; unsure what exists -> discover first.";
8597
9029
  ACTION_HELP = {
8598
9030
  search: "passages by meaning or keywords; an ID, code or number as the BARE identifier",
@@ -8615,7 +9047,7 @@ var init_knowledge_tool = __esm({
8615
9047
  graph: "the graph actions are off: this deployment has no graph configured, so nothing has been linked up. Use search, get_doc or list instead."
8616
9048
  };
8617
9049
  GRAPH_ACTIONS = ["traverse", "find_related", "get_neighbors", "community_summary"];
8618
- DOCUMENT_ID_ACTIONS = ["search", "compute", "get_docs", "map_reduce"];
9050
+ DOCUMENT_ID_ACTIONS = ["search", "compute", "get_docs", "map_reduce", "discover", "list"];
8619
9051
  DEFAULT_GET_DOCS_CHARS = 2e5;
8620
9052
  INPUT_PROPERTIES = {
8621
9053
  action: { type: "string", enum: [...KNOWLEDGE_ACTIONS], description: ACTION_PARAM_DESCRIPTION },
@@ -8635,7 +9067,7 @@ var init_knowledge_tool = __esm({
8635
9067
  document_ids: {
8636
9068
  type: "array",
8637
9069
  items: { type: "string" },
8638
- description: "Narrows to these documents, using ids from discover, list or a search hit. It INTERSECTS with source_ids, so a document outside the sources you named returns nothing. (for search, compute)"
9070
+ description: "Narrows to these documents, using ids from discover, list or a search hit. It INTERSECTS with source_ids, so a document outside the sources you named returns nothing. On discover it also asks for those documents' structure IN FULL, past the shortening a whole-page discover applies. (for search, compute, get_docs, map_reduce, discover, list)"
8639
9071
  },
8640
9072
  entity: {
8641
9073
  type: "string",
@@ -11390,7 +11822,7 @@ var SCHEMAS = {
11390
11822
  url: { type: "string", description: "Endpoint URL, may contain {path} params" },
11391
11823
  method: {
11392
11824
  type: "string",
11393
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
11825
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "QUERY"],
11394
11826
  default: "GET"
11395
11827
  },
11396
11828
  headers: {
@@ -11426,6 +11858,15 @@ var SCHEMAS = {
11426
11858
  llmQueryParameters: {
11427
11859
  type: "object",
11428
11860
  description: "LLM-filled parameters sent as the query string"
11861
+ },
11862
+ // How the response comes back to the caller, whatever the method. `tsv`
11863
+ // turns every array of objects in it into a TSV string (`formatResult`
11864
+ // in tools/response-mode.ts); a caller's explicit `responseMode` wins.
11865
+ response_mode: {
11866
+ type: "string",
11867
+ enum: ["json", "tsv"],
11868
+ default: "json",
11869
+ description: "Return the response as JSON, or its arrays of objects as TSV"
11429
11870
  }
11430
11871
  },
11431
11872
  required: ["url", "method"]
@@ -12591,6 +13032,89 @@ function findTool(tools, callName) {
12591
13032
  return tools.find((t) => t.callName === callName);
12592
13033
  }
12593
13034
 
13035
+ // src/tools/response-mode.ts
13036
+ var RESPONSE_MODES = ["json", "tsv"];
13037
+ function validateResponseMode(value, name = "responseMode") {
13038
+ if (!RESPONSE_MODES.includes(value)) {
13039
+ throw new Error(`${name} must be one of json, tsv, got ${JSON.stringify(value)}`);
13040
+ }
13041
+ return value;
13042
+ }
13043
+ function tsvCell(value) {
13044
+ if (value === null || value === void 0) return "\\N";
13045
+ let text;
13046
+ if (typeof value === "string") text = value;
13047
+ else if (value instanceof Date) text = value.toISOString();
13048
+ else if (typeof value === "bigint") text = value.toString();
13049
+ else {
13050
+ try {
13051
+ text = JSON.stringify(value) ?? String(value);
13052
+ } catch {
13053
+ text = String(value);
13054
+ }
13055
+ }
13056
+ return text.replaceAll("\\", "\\\\").replaceAll(" ", "\\t").replaceAll("\n", "\\n").replaceAll("\r", "\\r");
13057
+ }
13058
+ function isPlainObject2(value) {
13059
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
13060
+ const proto = Object.getPrototypeOf(value);
13061
+ return proto === Object.prototype || proto === null;
13062
+ }
13063
+ function isTable(value) {
13064
+ return Array.isArray(value) && value.length > 0 && value.every(isPlainObject2);
13065
+ }
13066
+ function tsvLines(rows) {
13067
+ const columns = [];
13068
+ const seen = /* @__PURE__ */ new Set();
13069
+ for (const row of rows) {
13070
+ for (const key of Object.keys(row)) {
13071
+ if (!seen.has(key)) {
13072
+ seen.add(key);
13073
+ columns.push(key);
13074
+ }
13075
+ }
13076
+ }
13077
+ return [
13078
+ columns.map(tsvCell).join(" "),
13079
+ ...rows.map((row) => columns.map((c) => tsvCell(row[c])).join(" "))
13080
+ ];
13081
+ }
13082
+ function rowsToTsv(rows) {
13083
+ return rows.length ? tsvLines(rows).join("\n") : "";
13084
+ }
13085
+ function collectTables(value, path, out) {
13086
+ if (isTable(value)) {
13087
+ out.set(JSON.stringify(path), { path, lines: tsvLines(value) });
13088
+ } else if (isPlainObject2(value)) {
13089
+ for (const [key, child] of Object.entries(value)) collectTables(child, [...path, key], out);
13090
+ } else if (Array.isArray(value)) {
13091
+ value.forEach((child, index) => {
13092
+ collectTables(child, [...path, index], out);
13093
+ });
13094
+ }
13095
+ }
13096
+ function buildTsv(value, path, tables, kept) {
13097
+ const id = JSON.stringify(path);
13098
+ const table = tables.get(id);
13099
+ if (table) return table.lines.slice(0, (kept.get(id) ?? 0) + 1).join("\n");
13100
+ if (isPlainObject2(value)) {
13101
+ const out = {};
13102
+ for (const [k, v] of Object.entries(value)) out[k] = buildTsv(v, [...path, k], tables, kept);
13103
+ return out;
13104
+ }
13105
+ if (Array.isArray(value)) return value.map((v, i) => buildTsv(v, [...path, i], tables, kept));
13106
+ return value;
13107
+ }
13108
+ function formatResult(result, responseMode = "json") {
13109
+ if (validateResponseMode(responseMode) === "json") return result;
13110
+ const root = { result };
13111
+ const tables = /* @__PURE__ */ new Map();
13112
+ collectTables(root, [], tables);
13113
+ if (!tables.size) return result;
13114
+ const kept = new Map([...tables].map(([id, t]) => [id, t.lines.length - 1]));
13115
+ return buildTsv(root, [], tables, kept).result;
13116
+ }
13117
+
12594
13118
  // src/tools/governance.ts
12595
13119
  var RESULT_MAX_CHARS = 8e3;
12596
13120
  var RESULT_MAX_ROWS = 100;
@@ -12622,12 +13146,90 @@ function stripUnderscoreArgs(args) {
12622
13146
  }
12623
13147
  return out;
12624
13148
  }
13149
+ function validateResultBudget(name, value, fallback) {
13150
+ if (value === void 0) return fallback;
13151
+ if (value === null) return null;
13152
+ if (typeof value !== "number" || !Number.isInteger(value)) {
13153
+ throw new TypeError(`${name} must be a positive integer or null, got ${typeof value}`);
13154
+ }
13155
+ if (value < 1) throw new Error(`${name} must be a positive integer or null, got ${value}`);
13156
+ return value;
13157
+ }
13158
+ function serialize(value) {
13159
+ try {
13160
+ return JSON.stringify(value) ?? JSON.stringify(String(value));
13161
+ } catch {
13162
+ return JSON.stringify(String(value));
13163
+ }
13164
+ }
13165
+ function clip(serialized, maxChars) {
13166
+ return {
13167
+ _truncated: serialized.slice(0, maxChars),
13168
+ _original_size: serialized.length,
13169
+ _note: "tool result exceeded the context budget and was truncated"
13170
+ };
13171
+ }
13172
+ function shapeTsv(result, maxChars, maxRows) {
13173
+ const wrapped = !isPlainObject2(result);
13174
+ const root = wrapped ? { result } : result;
13175
+ const tables = /* @__PURE__ */ new Map();
13176
+ collectTables(root, [], tables);
13177
+ if (!tables.size) return null;
13178
+ const sizes = new Map([...tables].map(([id, t]) => [id, t.lines.length - 1]));
13179
+ const kept = new Map([...sizes].map(([id, n]) => [id, maxRows === null ? n : Math.min(n, maxRows)]));
13180
+ const render = () => {
13181
+ const built = buildTsv(root, [], tables, kept);
13182
+ const notes = {};
13183
+ for (const [id, t] of tables) {
13184
+ const k = kept.get(id);
13185
+ const n = sizes.get(id);
13186
+ if (k < n) notes[t.path.join(".")] = { rows_returned: k, rows_omitted: n - k };
13187
+ }
13188
+ if (Object.keys(notes).length) {
13189
+ built._result_shaping = notes;
13190
+ return built;
13191
+ }
13192
+ return wrapped ? built.result : built;
13193
+ };
13194
+ const fits = () => maxChars === null || serialize(render()).length <= maxChars;
13195
+ const capped2 = [...tables.keys()].some((id) => kept.get(id) < sizes.get(id));
13196
+ if (fits()) return [render(), capped2];
13197
+ const initial = new Map(kept);
13198
+ const tableSize = (id) => tables.get(id).lines.slice(0, kept.get(id) + 1).join("\n").length;
13199
+ const bySize = [...tables.keys()].sort((a, b) => tableSize(b) - tableSize(a));
13200
+ for (const id of bySize) {
13201
+ let lo = 0;
13202
+ let hi = kept.get(id) - 1;
13203
+ let best = null;
13204
+ while (lo <= hi) {
13205
+ const mid = Math.floor((lo + hi) / 2);
13206
+ kept.set(id, mid);
13207
+ if (fits()) {
13208
+ best = mid;
13209
+ lo = mid + 1;
13210
+ } else {
13211
+ hi = mid - 1;
13212
+ }
13213
+ }
13214
+ if (best !== null) {
13215
+ kept.set(id, best);
13216
+ return [render(), true];
13217
+ }
13218
+ kept.set(id, 0);
13219
+ }
13220
+ for (const [id, n] of initial) kept.set(id, n);
13221
+ return [clip(serialize(render()), maxChars), true];
13222
+ }
12625
13223
  function shapeResult(result, opts = {}) {
12626
- const maxChars = opts.maxChars ?? RESULT_MAX_CHARS;
12627
- const maxRows = opts.maxRows ?? RESULT_MAX_ROWS;
13224
+ const maxChars = opts.maxChars === void 0 ? RESULT_MAX_CHARS : opts.maxChars;
13225
+ const maxRows = opts.maxRows === void 0 ? RESULT_MAX_ROWS : opts.maxRows;
13226
+ if (opts.responseMode === "tsv") {
13227
+ const tsv = shapeTsv(result, maxChars, maxRows);
13228
+ if (tsv) return tsv;
13229
+ }
12628
13230
  let truncated = false;
12629
13231
  let shaped = result;
12630
- if (result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
13232
+ if (maxRows !== null && result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
12631
13233
  const rows = result.rows;
12632
13234
  const kept = rows.slice(0, maxRows);
12633
13235
  shaped = {
@@ -12637,22 +13239,9 @@ function shapeResult(result, opts = {}) {
12637
13239
  };
12638
13240
  truncated = true;
12639
13241
  }
12640
- let serialized;
12641
- try {
12642
- serialized = JSON.stringify(shaped);
12643
- } catch {
12644
- serialized = JSON.stringify(String(shaped));
12645
- }
12646
- if (serialized.length > maxChars) {
12647
- return [
12648
- {
12649
- _truncated: serialized.slice(0, maxChars),
12650
- _original_size: serialized.length,
12651
- _note: "tool result exceeded the context budget and was truncated"
12652
- },
12653
- true
12654
- ];
12655
- }
13242
+ if (maxChars === null) return [shaped, truncated];
13243
+ const serialized = serialize(shaped);
13244
+ if (serialized.length > maxChars) return [clip(serialized, maxChars), true];
12656
13245
  return [shaped, truncated];
12657
13246
  }
12658
13247
  function redactToolResult(result, policy, opts) {
@@ -12763,9 +13352,29 @@ function canonicalToPublic(ct) {
12763
13352
  params_schema: ct.paramsSchema
12764
13353
  };
12765
13354
  }
13355
+ function configuredResponseMode(ct, config) {
13356
+ if (ct.kind !== "http" || config.response_mode == null) return "json";
13357
+ try {
13358
+ return validateResponseMode(config.response_mode, "config.response_mode");
13359
+ } catch {
13360
+ console.warn(
13361
+ `tool ${ct.callName} has an unknown config response_mode ${JSON.stringify(config.response_mode)}; returning json`
13362
+ );
13363
+ return "json";
13364
+ }
13365
+ }
13366
+ function checkConfigResponseMode(kind, config) {
13367
+ if (kind !== "http" || !config || config.response_mode == null) return;
13368
+ try {
13369
+ validateResponseMode(config.response_mode, "config.response_mode");
13370
+ } catch (exc) {
13371
+ throw new ConfigTemplateError(exc.message);
13372
+ }
13373
+ }
12766
13374
  async function registerTool(engine, tc) {
12767
13375
  canonicalFromConfig(tc);
12768
13376
  const config = tc.config ?? {};
13377
+ checkConfigResponseMode(tc.kind, config);
12769
13378
  if (containsSentinel(config)) {
12770
13379
  throw new ConfigTemplateError(
12771
13380
  `config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
@@ -12801,6 +13410,12 @@ async function updateTool(engine, id, opts) {
12801
13410
  if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, principals)) {
12802
13411
  throw new EngineActionError(`tool not found: ${id}`);
12803
13412
  }
13413
+ if ("config" in fields) {
13414
+ checkConfigResponseMode(
13415
+ "kind" in fields ? fields.kind : row.kind,
13416
+ fields.config
13417
+ );
13418
+ }
12804
13419
  const sets = [];
12805
13420
  const params = [];
12806
13421
  let i = 1;
@@ -13110,6 +13725,9 @@ function warnUnscopedApproval(callName) {
13110
13725
  async function executeTool(engine, callName, args, opts = {}) {
13111
13726
  const runtimeArgs = args ?? {};
13112
13727
  const approvalScope = validateApprovalScope(opts.approvalScope);
13728
+ const resultMaxChars = validateResultBudget("resultMaxChars", opts.resultMaxChars, RESULT_MAX_CHARS);
13729
+ const resultMaxRows = validateResultBudget("resultMaxRows", opts.resultMaxRows, RESULT_MAX_ROWS);
13730
+ let responseMode = opts.responseMode === void 0 || opts.responseMode === null ? null : validateResponseMode(opts.responseMode);
13113
13731
  const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
13114
13732
  if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
13115
13733
  if (!toolVisible(ct, opts.principals ?? null)) {
@@ -13152,6 +13770,7 @@ async function executeTool(engine, callName, args, opts = {}) {
13152
13770
  if (ct.kind !== "function" && ct.id != null) {
13153
13771
  config = await decryptCtConfig(engine, ct.id);
13154
13772
  }
13773
+ responseMode ??= configuredResponseMode(ct, config);
13155
13774
  const actorType = opts.actor?.type ?? null;
13156
13775
  const actorId = opts.actor?.id ?? null;
13157
13776
  let rawResult = null;
@@ -13175,7 +13794,16 @@ async function executeTool(engine, callName, args, opts = {}) {
13175
13794
  hooks: engine.hooks
13176
13795
  });
13177
13796
  rawResult = redacted;
13178
- [shaped, truncated] = shapeResult(rawResult);
13797
+ let toShape = rawResult;
13798
+ if (responseMode === "tsv" && ct.kind === "db" && isPlainObject2(rawResult)) {
13799
+ const { text: _preview, ...rest } = rawResult;
13800
+ toShape = rest;
13801
+ }
13802
+ [shaped, truncated] = shapeResult(toShape, {
13803
+ maxChars: resultMaxChars,
13804
+ maxRows: resultMaxRows,
13805
+ responseMode
13806
+ });
13179
13807
  } catch (e) {
13180
13808
  exc = e;
13181
13809
  success = false;
@@ -13668,6 +14296,7 @@ var ContextEngine = class _ContextEngine {
13668
14296
  pool,
13669
14297
  sourceId: opts.sourceId,
13670
14298
  sourceIds: opts.sourceIds,
14299
+ documentIds: opts.documentIds,
13671
14300
  principals: resolvePrincipals(opts.principals, "listDocuments"),
13672
14301
  cursor: opts.cursor,
13673
14302
  limit: opts.limit,
@@ -13676,6 +14305,84 @@ var ContextEngine = class _ContextEngine {
13676
14305
  hooks: this.hooks
13677
14306
  });
13678
14307
  }
14308
+ /**
14309
+ * Sheet names, columns and row counts for the named spreadsheets.
14310
+ *
14311
+ * See `actions.spreadsheetSchema`. This is the half of `discover` that lets
14312
+ * a model write ONE `compute` call: sheet names here are the keys it will
14313
+ * index `dfs` by, and columns are the names it will use inside the code it
14314
+ * writes.
14315
+ */
14316
+ async spreadsheetSchema(opts) {
14317
+ const pool = await this.ensurePool();
14318
+ return spreadsheetSchema({
14319
+ pool,
14320
+ documentIds: opts.documentIds,
14321
+ sourceIds: opts.sourceIds,
14322
+ principals: resolvePrincipals(opts.principals, "spreadsheetSchema"),
14323
+ redaction: opts.redaction ?? this.config.redaction,
14324
+ secretKey: this.config.secretKey,
14325
+ hooks: this.hooks
14326
+ });
14327
+ }
14328
+ /**
14329
+ * What is inside each of the named documents, whatever its type.
14330
+ *
14331
+ * See `actions.documentStructure`. Sheets and columns for a workbook,
14332
+ * sections and the last page for a document with headings, top-level keys
14333
+ * for JSON, and a chunk count for everything — so `discover` describes the
14334
+ * whole corpus rather than only the spreadsheets in it.
14335
+ */
14336
+ async documentStructure(opts) {
14337
+ const pool = await this.ensurePool();
14338
+ return documentStructure({
14339
+ pool,
14340
+ documentIds: opts.documentIds,
14341
+ sourceIds: opts.sourceIds,
14342
+ bounded: opts.bounded,
14343
+ principals: resolvePrincipals(opts.principals, "documentStructure"),
14344
+ redaction: opts.redaction ?? this.config.redaction,
14345
+ secretKey: this.config.secretKey,
14346
+ hooks: this.hooks
14347
+ });
14348
+ }
14349
+ /**
14350
+ * The corpus census — `[{kind, type, documents, with_fields}]`.
14351
+ *
14352
+ * See `actions.documentTypes`. `kind` comes from the mime and is always
14353
+ * known; `type` is the LLM-written document type and exists only where
14354
+ * structured extraction was opted into.
14355
+ */
14356
+ async documentTypes(opts = {}) {
14357
+ const pool = await this.ensurePool();
14358
+ return documentTypes({
14359
+ pool,
14360
+ sourceIds: opts.sourceIds,
14361
+ documentIds: opts.documentIds,
14362
+ principals: resolvePrincipals(opts.principals, "documentTypes"),
14363
+ redaction: opts.redaction ?? this.config.redaction,
14364
+ secretKey: this.config.secretKey,
14365
+ hooks: this.hooks
14366
+ });
14367
+ }
14368
+ /**
14369
+ * Extracted structured field names grouped by document kind and type.
14370
+ *
14371
+ * See `actions.fieldSummary`. One row per group, keyed by the same
14372
+ * `(kind, type)` pair `documentTypes` uses.
14373
+ */
14374
+ async fieldSummary(opts = {}) {
14375
+ const pool = await this.ensurePool();
14376
+ return fieldSummary({
14377
+ pool,
14378
+ sourceIds: opts.sourceIds,
14379
+ documentIds: opts.documentIds,
14380
+ principals: resolvePrincipals(opts.principals, "fieldSummary"),
14381
+ redaction: opts.redaction ?? this.config.redaction,
14382
+ secretKey: this.config.secretKey,
14383
+ hooks: this.hooks
14384
+ });
14385
+ }
13679
14386
  async queryStructured(question, opts = {}) {
13680
14387
  const pool = await this.ensurePool();
13681
14388
  return queryStructured(question, {
@@ -13752,7 +14459,10 @@ var ContextEngine = class _ContextEngine {
13752
14459
  principals: resolvePrincipals(opts.principals, "executeTool"),
13753
14460
  actor: opts.actor,
13754
14461
  source: opts.source ?? "api",
13755
- approvalScope: opts.approvalScope
14462
+ approvalScope: opts.approvalScope,
14463
+ resultMaxChars: opts.resultMaxChars,
14464
+ resultMaxRows: opts.resultMaxRows,
14465
+ responseMode: opts.responseMode
13756
14466
  });
13757
14467
  }
13758
14468
  };
@@ -14042,6 +14752,6 @@ init_sentinels();
14042
14752
  init_structured();
14043
14753
  init_usage();
14044
14754
 
14045
- export { ApprovalExpired, ApprovalNotPending, CeleryRunner, CodeExecutionError, CodeExecutionTimeout, ContextEngine, ContextEngineConfig, DEFAULT_LEG_WEIGHT, DocumentNotFoundError, EXTRACTION_VERSION, Embedder, EngineActionError, ExtraMissingError, Extracted, GraphLegUnavailable, InProcessRunner, KNOWLEDGE_ACTIONS, KNOWLEDGE_TOOL_DESCRIPTION, LLMClient, PostgresBackend, RedactionPolicy, RedactionRule, TRUSTED, ToolConfig, UNSCOPED, UNSET, __version__, applyRedaction, buildEmbedder, buildLlmClient, callKnowledgeTool, callLlm, compute, computeOverFrames, configSchema, createMcpApp, decryptDict, emitError, emitProgress, emitToolCall, emitUsage, encryptDict, extract2 as extract, extractStructuredData, functionTool, getDocumentText, getSecretKey, graphUnits, knowledgeToolDefinition, listDocuments, narrowToCeiling, queryStructured, redactHits, rerank, resolveApproval, resolveFields, resolvePrincipals, resolveScope, rrfFuse, runMigrate, runSearch, shouldRequireApproval, unitsForFile, upsertRegistry };
14755
+ export { ApprovalExpired, ApprovalNotPending, CeleryRunner, CodeExecutionError, CodeExecutionTimeout, ContextEngine, ContextEngineConfig, DEFAULT_LEG_WEIGHT, DocumentNotFoundError, EXTRACTION_VERSION, Embedder, EngineActionError, ExtraMissingError, Extracted, GraphLegUnavailable, InProcessRunner, KNOWLEDGE_ACTIONS, KNOWLEDGE_TOOL_DESCRIPTION, LLMClient, PostgresBackend, RedactionPolicy, RedactionRule, TRUSTED, ToolConfig, UNSCOPED, UNSET, __version__, applyRedaction, buildEmbedder, buildLlmClient, callKnowledgeTool, callLlm, compute, computeOverFrames, configSchema, createMcpApp, decryptDict, documentStructure, documentTypes, emitError, emitProgress, emitToolCall, emitUsage, encryptDict, extract2 as extract, extractStructuredData, fieldSummary, formatResult, functionTool, getDocumentText, getSecretKey, graphUnits, knowledgeToolDefinition, listDocuments, narrowToCeiling, queryStructured, redactHits, rerank, resolveApproval, resolveFields, resolvePrincipals, resolveScope, rowsToTsv, rrfFuse, runMigrate, runSearch, shouldRequireApproval, spreadsheetSchema, spreadsheetSchemaFromText, unitsForFile, upsertRegistry };
14046
14756
  //# sourceMappingURL=index.js.map
14047
14757
  //# sourceMappingURL=index.js.map