@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/fastify.cjs CHANGED
@@ -947,7 +947,7 @@ var SCHEMAS = {
947
947
  url: { type: "string", description: "Endpoint URL, may contain {path} params" },
948
948
  method: {
949
949
  type: "string",
950
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
950
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "QUERY"],
951
951
  default: "GET"
952
952
  },
953
953
  headers: {
@@ -983,6 +983,15 @@ var SCHEMAS = {
983
983
  llmQueryParameters: {
984
984
  type: "object",
985
985
  description: "LLM-filled parameters sent as the query string"
986
+ },
987
+ // How the response comes back to the caller, whatever the method. `tsv`
988
+ // turns every array of objects in it into a TSV string (`formatResult`
989
+ // in tools/response-mode.ts); a caller's explicit `responseMode` wins.
990
+ response_mode: {
991
+ type: "string",
992
+ enum: ["json", "tsv"],
993
+ default: "json",
994
+ description: "Return the response as JSON, or its arrays of objects as TSV"
986
995
  }
987
996
  },
988
997
  required: ["url", "method"]
@@ -2510,6 +2519,77 @@ function findTool(tools, callName) {
2510
2519
  return tools.find((t) => t.callName === callName);
2511
2520
  }
2512
2521
 
2522
+ // src/tools/response-mode.ts
2523
+ var RESPONSE_MODES = ["json", "tsv"];
2524
+ function validateResponseMode(value, name = "responseMode") {
2525
+ if (!RESPONSE_MODES.includes(value)) {
2526
+ throw new Error(`${name} must be one of json, tsv, got ${JSON.stringify(value)}`);
2527
+ }
2528
+ return value;
2529
+ }
2530
+ function tsvCell(value) {
2531
+ if (value === null || value === void 0) return "\\N";
2532
+ let text;
2533
+ if (typeof value === "string") text = value;
2534
+ else if (value instanceof Date) text = value.toISOString();
2535
+ else if (typeof value === "bigint") text = value.toString();
2536
+ else {
2537
+ try {
2538
+ text = JSON.stringify(value) ?? String(value);
2539
+ } catch {
2540
+ text = String(value);
2541
+ }
2542
+ }
2543
+ return text.replaceAll("\\", "\\\\").replaceAll(" ", "\\t").replaceAll("\n", "\\n").replaceAll("\r", "\\r");
2544
+ }
2545
+ function isPlainObject2(value) {
2546
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2547
+ const proto = Object.getPrototypeOf(value);
2548
+ return proto === Object.prototype || proto === null;
2549
+ }
2550
+ function isTable(value) {
2551
+ return Array.isArray(value) && value.length > 0 && value.every(isPlainObject2);
2552
+ }
2553
+ function tsvLines(rows) {
2554
+ const columns = [];
2555
+ const seen = /* @__PURE__ */ new Set();
2556
+ for (const row of rows) {
2557
+ for (const key of Object.keys(row)) {
2558
+ if (!seen.has(key)) {
2559
+ seen.add(key);
2560
+ columns.push(key);
2561
+ }
2562
+ }
2563
+ }
2564
+ return [
2565
+ columns.map(tsvCell).join(" "),
2566
+ ...rows.map((row) => columns.map((c) => tsvCell(row[c])).join(" "))
2567
+ ];
2568
+ }
2569
+ function collectTables(value, path, out) {
2570
+ if (isTable(value)) {
2571
+ out.set(JSON.stringify(path), { path, lines: tsvLines(value) });
2572
+ } else if (isPlainObject2(value)) {
2573
+ for (const [key, child] of Object.entries(value)) collectTables(child, [...path, key], out);
2574
+ } else if (Array.isArray(value)) {
2575
+ value.forEach((child, index) => {
2576
+ collectTables(child, [...path, index], out);
2577
+ });
2578
+ }
2579
+ }
2580
+ function buildTsv(value, path, tables, kept) {
2581
+ const id = JSON.stringify(path);
2582
+ const table = tables.get(id);
2583
+ if (table) return table.lines.slice(0, (kept.get(id) ?? 0) + 1).join("\n");
2584
+ if (isPlainObject2(value)) {
2585
+ const out = {};
2586
+ for (const [k, v] of Object.entries(value)) out[k] = buildTsv(v, [...path, k], tables, kept);
2587
+ return out;
2588
+ }
2589
+ if (Array.isArray(value)) return value.map((v, i) => buildTsv(v, [...path, i], tables, kept));
2590
+ return value;
2591
+ }
2592
+
2513
2593
  // src/tools/governance.ts
2514
2594
  var RESULT_MAX_CHARS = 8e3;
2515
2595
  var RESULT_MAX_ROWS = 100;
@@ -2541,12 +2621,90 @@ function stripUnderscoreArgs(args) {
2541
2621
  }
2542
2622
  return out;
2543
2623
  }
2624
+ function validateResultBudget(name, value, fallback) {
2625
+ if (value === void 0) return fallback;
2626
+ if (value === null) return null;
2627
+ if (typeof value !== "number" || !Number.isInteger(value)) {
2628
+ throw new TypeError(`${name} must be a positive integer or null, got ${typeof value}`);
2629
+ }
2630
+ if (value < 1) throw new Error(`${name} must be a positive integer or null, got ${value}`);
2631
+ return value;
2632
+ }
2633
+ function serialize(value) {
2634
+ try {
2635
+ return JSON.stringify(value) ?? JSON.stringify(String(value));
2636
+ } catch {
2637
+ return JSON.stringify(String(value));
2638
+ }
2639
+ }
2640
+ function clip(serialized, maxChars) {
2641
+ return {
2642
+ _truncated: serialized.slice(0, maxChars),
2643
+ _original_size: serialized.length,
2644
+ _note: "tool result exceeded the context budget and was truncated"
2645
+ };
2646
+ }
2647
+ function shapeTsv(result, maxChars, maxRows) {
2648
+ const wrapped = !isPlainObject2(result);
2649
+ const root = wrapped ? { result } : result;
2650
+ const tables = /* @__PURE__ */ new Map();
2651
+ collectTables(root, [], tables);
2652
+ if (!tables.size) return null;
2653
+ const sizes = new Map([...tables].map(([id, t]) => [id, t.lines.length - 1]));
2654
+ const kept = new Map([...sizes].map(([id, n]) => [id, maxRows === null ? n : Math.min(n, maxRows)]));
2655
+ const render = () => {
2656
+ const built = buildTsv(root, [], tables, kept);
2657
+ const notes = {};
2658
+ for (const [id, t] of tables) {
2659
+ const k = kept.get(id);
2660
+ const n = sizes.get(id);
2661
+ if (k < n) notes[t.path.join(".")] = { rows_returned: k, rows_omitted: n - k };
2662
+ }
2663
+ if (Object.keys(notes).length) {
2664
+ built._result_shaping = notes;
2665
+ return built;
2666
+ }
2667
+ return wrapped ? built.result : built;
2668
+ };
2669
+ const fits = () => maxChars === null || serialize(render()).length <= maxChars;
2670
+ const capped = [...tables.keys()].some((id) => kept.get(id) < sizes.get(id));
2671
+ if (fits()) return [render(), capped];
2672
+ const initial = new Map(kept);
2673
+ const tableSize = (id) => tables.get(id).lines.slice(0, kept.get(id) + 1).join("\n").length;
2674
+ const bySize = [...tables.keys()].sort((a, b) => tableSize(b) - tableSize(a));
2675
+ for (const id of bySize) {
2676
+ let lo = 0;
2677
+ let hi = kept.get(id) - 1;
2678
+ let best = null;
2679
+ while (lo <= hi) {
2680
+ const mid = Math.floor((lo + hi) / 2);
2681
+ kept.set(id, mid);
2682
+ if (fits()) {
2683
+ best = mid;
2684
+ lo = mid + 1;
2685
+ } else {
2686
+ hi = mid - 1;
2687
+ }
2688
+ }
2689
+ if (best !== null) {
2690
+ kept.set(id, best);
2691
+ return [render(), true];
2692
+ }
2693
+ kept.set(id, 0);
2694
+ }
2695
+ for (const [id, n] of initial) kept.set(id, n);
2696
+ return [clip(serialize(render()), maxChars), true];
2697
+ }
2544
2698
  function shapeResult(result, opts = {}) {
2545
- const maxChars = opts.maxChars ?? RESULT_MAX_CHARS;
2546
- const maxRows = opts.maxRows ?? RESULT_MAX_ROWS;
2699
+ const maxChars = opts.maxChars === void 0 ? RESULT_MAX_CHARS : opts.maxChars;
2700
+ const maxRows = opts.maxRows === void 0 ? RESULT_MAX_ROWS : opts.maxRows;
2701
+ if (opts.responseMode === "tsv") {
2702
+ const tsv = shapeTsv(result, maxChars, maxRows);
2703
+ if (tsv) return tsv;
2704
+ }
2547
2705
  let truncated = false;
2548
2706
  let shaped = result;
2549
- if (result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
2707
+ if (maxRows !== null && result && typeof result === "object" && Array.isArray(result.rows) && result.rows.length > maxRows) {
2550
2708
  const rows = result.rows;
2551
2709
  const kept = rows.slice(0, maxRows);
2552
2710
  shaped = {
@@ -2556,22 +2714,9 @@ function shapeResult(result, opts = {}) {
2556
2714
  };
2557
2715
  truncated = true;
2558
2716
  }
2559
- let serialized;
2560
- try {
2561
- serialized = JSON.stringify(shaped);
2562
- } catch {
2563
- serialized = JSON.stringify(String(shaped));
2564
- }
2565
- if (serialized.length > maxChars) {
2566
- return [
2567
- {
2568
- _truncated: serialized.slice(0, maxChars),
2569
- _original_size: serialized.length,
2570
- _note: "tool result exceeded the context budget and was truncated"
2571
- },
2572
- true
2573
- ];
2574
- }
2717
+ if (maxChars === null) return [shaped, truncated];
2718
+ const serialized = serialize(shaped);
2719
+ if (serialized.length > maxChars) return [clip(serialized, maxChars), true];
2575
2720
  return [shaped, truncated];
2576
2721
  }
2577
2722
  function redactToolResult(result, policy, opts) {
@@ -2674,9 +2819,29 @@ async function mergedTools(engine, sourceId) {
2674
2819
  const functions = Object.values(engine._functionTools ?? {});
2675
2820
  return [...persisted, ...functions];
2676
2821
  }
2822
+ function configuredResponseMode(ct, config) {
2823
+ if (ct.kind !== "http" || config.response_mode == null) return "json";
2824
+ try {
2825
+ return validateResponseMode(config.response_mode, "config.response_mode");
2826
+ } catch {
2827
+ console.warn(
2828
+ `tool ${ct.callName} has an unknown config response_mode ${JSON.stringify(config.response_mode)}; returning json`
2829
+ );
2830
+ return "json";
2831
+ }
2832
+ }
2833
+ function checkConfigResponseMode(kind, config) {
2834
+ if (kind !== "http" || !config || config.response_mode == null) return;
2835
+ try {
2836
+ validateResponseMode(config.response_mode, "config.response_mode");
2837
+ } catch (exc) {
2838
+ throw new ConfigTemplateError(exc.message);
2839
+ }
2840
+ }
2677
2841
  async function registerTool(engine, tc) {
2678
2842
  canonicalFromConfig(tc);
2679
2843
  const config = tc.config ?? {};
2844
+ checkConfigResponseMode(tc.kind, config);
2680
2845
  if (containsSentinel(config)) {
2681
2846
  throw new ConfigTemplateError(
2682
2847
  `config contains the ${JSON.stringify(REDACTED_SENTINEL)} placeholder \u2014 a redacted template cannot be registered as a new tool; re-enter the secret values`
@@ -2712,6 +2877,12 @@ async function updateTool(engine, id, opts) {
2712
2877
  if (!row || !aclVisible(row.acl != null ? [...row.acl] : null, principals)) {
2713
2878
  throw new EngineActionError(`tool not found: ${id}`);
2714
2879
  }
2880
+ if ("config" in fields) {
2881
+ checkConfigResponseMode(
2882
+ "kind" in fields ? fields.kind : row.kind,
2883
+ fields.config
2884
+ );
2885
+ }
2715
2886
  const sets = [];
2716
2887
  const params = [];
2717
2888
  let i = 1;
@@ -3002,6 +3173,9 @@ function warnUnscopedApproval(callName) {
3002
3173
  async function executeTool(engine, callName, args, opts = {}) {
3003
3174
  const runtimeArgs = args ?? {};
3004
3175
  const approvalScope = validateApprovalScope(opts.approvalScope);
3176
+ const resultMaxChars = validateResultBudget("resultMaxChars", opts.resultMaxChars, RESULT_MAX_CHARS);
3177
+ const resultMaxRows = validateResultBudget("resultMaxRows", opts.resultMaxRows, RESULT_MAX_ROWS);
3178
+ let responseMode = opts.responseMode === void 0 || opts.responseMode === null ? null : validateResponseMode(opts.responseMode);
3005
3179
  const ct = findTool(await mergedTools(engine, opts.sourceId ?? null), callName);
3006
3180
  if (!ct) throw new EngineActionError(`tool not found: ${callName}`);
3007
3181
  if (!toolVisible(ct, opts.principals ?? null)) {
@@ -3044,6 +3218,7 @@ async function executeTool(engine, callName, args, opts = {}) {
3044
3218
  if (ct.kind !== "function" && ct.id != null) {
3045
3219
  config = await decryptCtConfig(engine, ct.id);
3046
3220
  }
3221
+ responseMode ??= configuredResponseMode(ct, config);
3047
3222
  const actorType = opts.actor?.type ?? null;
3048
3223
  const actorId = opts.actor?.id ?? null;
3049
3224
  let rawResult = null;
@@ -3067,7 +3242,16 @@ async function executeTool(engine, callName, args, opts = {}) {
3067
3242
  hooks: engine.hooks
3068
3243
  });
3069
3244
  rawResult = redacted;
3070
- [shaped, truncated] = shapeResult(rawResult);
3245
+ let toShape = rawResult;
3246
+ if (responseMode === "tsv" && ct.kind === "db" && isPlainObject2(rawResult)) {
3247
+ const { text: _preview, ...rest } = rawResult;
3248
+ toShape = rest;
3249
+ }
3250
+ [shaped, truncated] = shapeResult(toShape, {
3251
+ maxChars: resultMaxChars,
3252
+ maxRows: resultMaxRows,
3253
+ responseMode
3254
+ });
3071
3255
  } catch (e) {
3072
3256
  exc = e;
3073
3257
  success = false;