@promptev/context-engine 0.0.4 → 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/README.md CHANGED
@@ -296,6 +296,46 @@ record. Resolve it server-side — on the adapters and the MCP gateway it is an
296
296
  still works this release, unscoped and with a `DeprecationWarning`; the next
297
297
  release refuses a gated call with no scope.
298
298
 
299
+ **Result budget** — the returned `result` is cut to 8,000 characters and 100
300
+ rows unless you say otherwise. Size it to your model's window, or pass `null`
301
+ to lift a limit. `responseMode: "tsv"` works for **any** tool: every array of
302
+ objects in the result — a db tool's `rows`, an HTTP API's `data.items`, a
303
+ function's returned list — comes back as a TSV string under the same key, and
304
+ the budget cuts at whole rows:
305
+
306
+ ```ts
307
+ const out = await engine.executeTool("db_sales", { query: "SELECT * FROM orders" }, {
308
+ principals: ["user:a"],
309
+ resultMaxChars: 200_000, // null = no character limit
310
+ resultMaxRows: null, // per table; a db tool's own max_rows (default 1000) still applies
311
+ responseMode: "tsv", // -> { success: true, columns: [...], rows: "id\tname\n1\t..." }
312
+ });
313
+ // a cut reports where: { _result_shaping: { rows: { rows_returned: 812, rows_omitted: 188 } } }
314
+ ```
315
+
316
+ In TSV, `\N` is NULL, nested values are compact JSON cells, and tab, newline,
317
+ CR and backslash inside a value are backslash-escaped. Parts of a result that
318
+ aren't arrays of objects stay JSON; a result that *is* one becomes the string
319
+ (wrapped as `{ result, _result_shaping }` when cut). The audit row always keeps
320
+ the original under its own 50KB cap, whatever you pass.
321
+
322
+ An **http tool** can set its format once, in its config — for every method
323
+ (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `QUERY`). Omitted, it is `json`; a
324
+ caller's explicit `responseMode` still wins:
325
+
326
+ ```ts
327
+ await engine.registerTool(new ToolConfig({
328
+ name: "orders",
329
+ kind: "http",
330
+ config: { method: "QUERY", url: "https://api.example.com/orders", response_mode: "tsv" }, // "json" (default) | "tsv"
331
+ acl: ["group:ops"],
332
+ }));
333
+ ```
334
+
335
+ The same conversion is a plain function for anything else:
336
+ `formatResult(result, "tsv")` (and `rowsToTsv(rows)`) from
337
+ `@promptev/context-engine` — `"json"`, the default, returns the input unchanged.
338
+
299
339
  A caller may only register or relabel a tool under principals it holds — filing
300
340
  one under another group's ACL is a 403.
301
341
 
package/dist/cli.js CHANGED
@@ -12554,6 +12554,82 @@ var init_registry = __esm({
12554
12554
  "src/tools/registry.ts"() {
12555
12555
  }
12556
12556
  });
12557
+
12558
+ // src/tools/response-mode.ts
12559
+ function validateResponseMode(value, name = "responseMode") {
12560
+ if (!RESPONSE_MODES.includes(value)) {
12561
+ throw new Error(`${name} must be one of json, tsv, got ${JSON.stringify(value)}`);
12562
+ }
12563
+ return value;
12564
+ }
12565
+ function tsvCell(value) {
12566
+ if (value === null || value === void 0) return "\\N";
12567
+ let text;
12568
+ if (typeof value === "string") text = value;
12569
+ else if (value instanceof Date) text = value.toISOString();
12570
+ else if (typeof value === "bigint") text = value.toString();
12571
+ else {
12572
+ try {
12573
+ text = JSON.stringify(value) ?? String(value);
12574
+ } catch {
12575
+ text = String(value);
12576
+ }
12577
+ }
12578
+ return text.replaceAll("\\", "\\\\").replaceAll(" ", "\\t").replaceAll("\n", "\\n").replaceAll("\r", "\\r");
12579
+ }
12580
+ function isPlainObject2(value) {
12581
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
12582
+ const proto = Object.getPrototypeOf(value);
12583
+ return proto === Object.prototype || proto === null;
12584
+ }
12585
+ function isTable(value) {
12586
+ return Array.isArray(value) && value.length > 0 && value.every(isPlainObject2);
12587
+ }
12588
+ function tsvLines(rows) {
12589
+ const columns = [];
12590
+ const seen = /* @__PURE__ */ new Set();
12591
+ for (const row of rows) {
12592
+ for (const key of Object.keys(row)) {
12593
+ if (!seen.has(key)) {
12594
+ seen.add(key);
12595
+ columns.push(key);
12596
+ }
12597
+ }
12598
+ }
12599
+ return [
12600
+ columns.map(tsvCell).join(" "),
12601
+ ...rows.map((row) => columns.map((c) => tsvCell(row[c])).join(" "))
12602
+ ];
12603
+ }
12604
+ function collectTables(value, path, out) {
12605
+ if (isTable(value)) {
12606
+ out.set(JSON.stringify(path), { path, lines: tsvLines(value) });
12607
+ } else if (isPlainObject2(value)) {
12608
+ for (const [key, child] of Object.entries(value)) collectTables(child, [...path, key], out);
12609
+ } else if (Array.isArray(value)) {
12610
+ value.forEach((child, index) => {
12611
+ collectTables(child, [...path, index], out);
12612
+ });
12613
+ }
12614
+ }
12615
+ function buildTsv(value, path, tables, kept) {
12616
+ const id = JSON.stringify(path);
12617
+ const table = tables.get(id);
12618
+ if (table) return table.lines.slice(0, (kept.get(id) ?? 0) + 1).join("\n");
12619
+ if (isPlainObject2(value)) {
12620
+ const out = {};
12621
+ for (const [k, v] of Object.entries(value)) out[k] = buildTsv(v, [...path, k], tables, kept);
12622
+ return out;
12623
+ }
12624
+ if (Array.isArray(value)) return value.map((v, i) => buildTsv(v, [...path, i], tables, kept));
12625
+ return value;
12626
+ }
12627
+ var RESPONSE_MODES;
12628
+ var init_response_mode = __esm({
12629
+ "src/tools/response-mode.ts"() {
12630
+ RESPONSE_MODES = ["json", "tsv"];
12631
+ }
12632
+ });
12557
12633
  function allowPrivateEgress(engine) {
12558
12634
  return Boolean(engine?.config?.allowPrivateEgress);
12559
12635
  }
@@ -12567,12 +12643,90 @@ function stripUnderscoreArgs(args) {
12567
12643
  }
12568
12644
  return out;
12569
12645
  }
12646
+ function validateResultBudget(name, value, fallback) {
12647
+ if (value === void 0) return fallback;
12648
+ if (value === null) return null;
12649
+ if (typeof value !== "number" || !Number.isInteger(value)) {
12650
+ throw new TypeError(`${name} must be a positive integer or null, got ${typeof value}`);
12651
+ }
12652
+ if (value < 1) throw new Error(`${name} must be a positive integer or null, got ${value}`);
12653
+ return value;
12654
+ }
12655
+ function serialize(value) {
12656
+ try {
12657
+ return JSON.stringify(value) ?? JSON.stringify(String(value));
12658
+ } catch {
12659
+ return JSON.stringify(String(value));
12660
+ }
12661
+ }
12662
+ function clip(serialized, maxChars) {
12663
+ return {
12664
+ _truncated: serialized.slice(0, maxChars),
12665
+ _original_size: serialized.length,
12666
+ _note: "tool result exceeded the context budget and was truncated"
12667
+ };
12668
+ }
12669
+ function shapeTsv(result, maxChars, maxRows) {
12670
+ const wrapped = !isPlainObject2(result);
12671
+ const root = wrapped ? { result } : result;
12672
+ const tables = /* @__PURE__ */ new Map();
12673
+ collectTables(root, [], tables);
12674
+ if (!tables.size) return null;
12675
+ const sizes = new Map([...tables].map(([id, t]) => [id, t.lines.length - 1]));
12676
+ const kept = new Map([...sizes].map(([id, n]) => [id, maxRows === null ? n : Math.min(n, maxRows)]));
12677
+ const render = () => {
12678
+ const built = buildTsv(root, [], tables, kept);
12679
+ const notes = {};
12680
+ for (const [id, t] of tables) {
12681
+ const k = kept.get(id);
12682
+ const n = sizes.get(id);
12683
+ if (k < n) notes[t.path.join(".")] = { rows_returned: k, rows_omitted: n - k };
12684
+ }
12685
+ if (Object.keys(notes).length) {
12686
+ built._result_shaping = notes;
12687
+ return built;
12688
+ }
12689
+ return wrapped ? built.result : built;
12690
+ };
12691
+ const fits = () => maxChars === null || serialize(render()).length <= maxChars;
12692
+ const capped2 = [...tables.keys()].some((id) => kept.get(id) < sizes.get(id));
12693
+ if (fits()) return [render(), capped2];
12694
+ const initial = new Map(kept);
12695
+ const tableSize = (id) => tables.get(id).lines.slice(0, kept.get(id) + 1).join("\n").length;
12696
+ const bySize = [...tables.keys()].sort((a, b) => tableSize(b) - tableSize(a));
12697
+ for (const id of bySize) {
12698
+ let lo = 0;
12699
+ let hi = kept.get(id) - 1;
12700
+ let best = null;
12701
+ while (lo <= hi) {
12702
+ const mid = Math.floor((lo + hi) / 2);
12703
+ kept.set(id, mid);
12704
+ if (fits()) {
12705
+ best = mid;
12706
+ lo = mid + 1;
12707
+ } else {
12708
+ hi = mid - 1;
12709
+ }
12710
+ }
12711
+ if (best !== null) {
12712
+ kept.set(id, best);
12713
+ return [render(), true];
12714
+ }
12715
+ kept.set(id, 0);
12716
+ }
12717
+ for (const [id, n] of initial) kept.set(id, n);
12718
+ return [clip(serialize(render()), maxChars), true];
12719
+ }
12570
12720
  function shapeResult(result, opts = {}) {
12571
- const maxChars = opts.maxChars ?? RESULT_MAX_CHARS;
12572
- const maxRows = opts.maxRows ?? RESULT_MAX_ROWS;
12721
+ const maxChars = opts.maxChars === void 0 ? RESULT_MAX_CHARS : opts.maxChars;
12722
+ const maxRows = opts.maxRows === void 0 ? RESULT_MAX_ROWS : opts.maxRows;
12723
+ if (opts.responseMode === "tsv") {
12724
+ const tsv = shapeTsv(result, maxChars, maxRows);
12725
+ if (tsv) return tsv;
12726
+ }
12573
12727
  let truncated = false;
12574
12728
  let shaped = result;
12575
- if (result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
12729
+ if (maxRows !== null && result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
12576
12730
  const rows = result.rows;
12577
12731
  const kept = rows.slice(0, maxRows);
12578
12732
  shaped = {
@@ -12582,22 +12736,9 @@ function shapeResult(result, opts = {}) {
12582
12736
  };
12583
12737
  truncated = true;
12584
12738
  }
12585
- let serialized;
12586
- try {
12587
- serialized = JSON.stringify(shaped);
12588
- } catch {
12589
- serialized = JSON.stringify(String(shaped));
12590
- }
12591
- if (serialized.length > maxChars) {
12592
- return [
12593
- {
12594
- _truncated: serialized.slice(0, maxChars),
12595
- _original_size: serialized.length,
12596
- _note: "tool result exceeded the context budget and was truncated"
12597
- },
12598
- true
12599
- ];
12600
- }
12739
+ if (maxChars === null) return [shaped, truncated];
12740
+ const serialized = serialize(shaped);
12741
+ if (serialized.length > maxChars) return [clip(serialized, maxChars), true];
12601
12742
  return [shaped, truncated];
12602
12743
  }
12603
12744
  function redactToolResult(result, policy, opts) {
@@ -12708,9 +12849,29 @@ function canonicalToPublic(ct) {
12708
12849
  params_schema: ct.paramsSchema
12709
12850
  };
12710
12851
  }
12852
+ function configuredResponseMode(ct, config) {
12853
+ if (ct.kind !== "http" || config.response_mode == null) return "json";
12854
+ try {
12855
+ return validateResponseMode(config.response_mode, "config.response_mode");
12856
+ } catch {
12857
+ console.warn(
12858
+ `tool ${ct.callName} has an unknown config response_mode ${JSON.stringify(config.response_mode)}; returning json`
12859
+ );
12860
+ return "json";
12861
+ }
12862
+ }
12863
+ function checkConfigResponseMode(kind, config) {
12864
+ if (kind !== "http" || !config || config.response_mode == null) return;
12865
+ try {
12866
+ validateResponseMode(config.response_mode, "config.response_mode");
12867
+ } catch (exc) {
12868
+ throw new ConfigTemplateError(exc.message);
12869
+ }
12870
+ }
12711
12871
  async function registerTool(engine, tc) {
12712
12872
  canonicalFromConfig(tc);
12713
12873
  const config = tc.config ?? {};
12874
+ checkConfigResponseMode(tc.kind, config);
12714
12875
  if (containsSentinel(config)) {
12715
12876
  throw new ConfigTemplateError(
12716
12877
  `config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
@@ -12746,6 +12907,12 @@ async function updateTool(engine, id, opts) {
12746
12907
  if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, principals)) {
12747
12908
  throw new EngineActionError(`tool not found: ${id}`);
12748
12909
  }
12910
+ if ("config" in fields) {
12911
+ checkConfigResponseMode(
12912
+ "kind" in fields ? fields.kind : row.kind,
12913
+ fields.config
12914
+ );
12915
+ }
12749
12916
  const sets = [];
12750
12917
  const params = [];
12751
12918
  let i = 1;
@@ -13017,6 +13184,9 @@ function warnUnscopedApproval(callName) {
13017
13184
  async function executeTool(engine, callName, args, opts = {}) {
13018
13185
  const runtimeArgs = args ?? {};
13019
13186
  const approvalScope = validateApprovalScope(opts.approvalScope);
13187
+ const resultMaxChars = validateResultBudget("resultMaxChars", opts.resultMaxChars, RESULT_MAX_CHARS);
13188
+ const resultMaxRows = validateResultBudget("resultMaxRows", opts.resultMaxRows, RESULT_MAX_ROWS);
13189
+ let responseMode = opts.responseMode === void 0 || opts.responseMode === null ? null : validateResponseMode(opts.responseMode);
13020
13190
  const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
13021
13191
  if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
13022
13192
  if (!toolVisible(ct, opts.principals ?? null)) {
@@ -13059,6 +13229,7 @@ async function executeTool(engine, callName, args, opts = {}) {
13059
13229
  if (ct.kind !== "function" && ct.id != null) {
13060
13230
  config = await decryptCtConfig(engine, ct.id);
13061
13231
  }
13232
+ responseMode ??= configuredResponseMode(ct, config);
13062
13233
  const actorType = opts.actor?.type ?? null;
13063
13234
  const actorId = opts.actor?.id ?? null;
13064
13235
  let rawResult = null;
@@ -13082,7 +13253,16 @@ async function executeTool(engine, callName, args, opts = {}) {
13082
13253
  hooks: engine.hooks
13083
13254
  });
13084
13255
  rawResult = redacted;
13085
- [shaped, truncated] = shapeResult(rawResult);
13256
+ let toShape = rawResult;
13257
+ if (responseMode === "tsv" && ct.kind === "db" && isPlainObject2(rawResult)) {
13258
+ const { text: _preview, ...rest } = rawResult;
13259
+ toShape = rest;
13260
+ }
13261
+ [shaped, truncated] = shapeResult(toShape, {
13262
+ maxChars: resultMaxChars,
13263
+ maxRows: resultMaxRows,
13264
+ responseMode
13265
+ });
13086
13266
  } catch (e) {
13087
13267
  exc = e;
13088
13268
  success = false;
@@ -13134,6 +13314,7 @@ var init_governance = __esm({
13134
13314
  init_http();
13135
13315
  init_mcp_client();
13136
13316
  init_registry();
13317
+ init_response_mode();
13137
13318
  RESULT_MAX_CHARS = 8e3;
13138
13319
  RESULT_MAX_ROWS = 100;
13139
13320
  UPDATABLE_COLUMNS = /* @__PURE__ */ new Set([
@@ -14415,7 +14596,10 @@ var init_engine = __esm({
14415
14596
  principals: resolvePrincipals(opts.principals, "executeTool"),
14416
14597
  actor: opts.actor,
14417
14598
  source: opts.source ?? "api",
14418
- approvalScope: opts.approvalScope
14599
+ approvalScope: opts.approvalScope,
14600
+ resultMaxChars: opts.resultMaxChars,
14601
+ resultMaxRows: opts.resultMaxRows,
14602
+ responseMode: opts.responseMode
14419
14603
  });
14420
14604
  }
14421
14605
  };