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