@theoremjs/agents 0.3.4-next.484.g20d8b9d → 0.3.4-next.522.g9e684cd

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.
Files changed (95) hide show
  1. package/README.md +61 -51
  2. package/esm/mod.d.ts +2 -2
  3. package/esm/mod.js +1 -1
  4. package/esm/src/guardrails/credential-scan.d.ts +1 -1
  5. package/esm/src/guardrails/credential-scan.js +41 -18
  6. package/esm/src/guardrails/destinations.js +6 -2
  7. package/esm/src/guardrails/detect-at.d.ts +5 -5
  8. package/esm/src/guardrails/detect-at.js +51 -41
  9. package/esm/src/guardrails/detect-reply.d.ts +1 -1
  10. package/esm/src/guardrails/detect-reply.js +4 -2
  11. package/esm/src/guardrails/egress-automata.js +203 -202
  12. package/esm/src/guardrails/egress-compiler.js +1 -4
  13. package/esm/src/guardrails/egress-stream.js +10 -3
  14. package/esm/src/guardrails/egress-urls.js +4 -2
  15. package/esm/src/guardrails/error.d.ts +1 -1
  16. package/esm/src/guardrails/error.js +19 -2
  17. package/esm/src/guardrails/host-patterns.d.ts +1 -4
  18. package/esm/src/guardrails/host-patterns.js +1 -10
  19. package/esm/src/guardrails/injection-patterns.d.ts +3 -1
  20. package/esm/src/guardrails/injection-patterns.js +87 -4
  21. package/esm/src/guardrails/labels.d.ts +2 -2
  22. package/esm/src/guardrails/labels.js +7 -5
  23. package/esm/src/guardrails/mod.d.ts +2 -2
  24. package/esm/src/guardrails/mod.js +1 -1
  25. package/esm/src/guardrails/normalize.js +4 -2
  26. package/esm/src/guardrails/prompt-echo.js +4 -2
  27. package/esm/src/guardrails/sanitize.d.ts +1 -1
  28. package/esm/src/guardrails/sanitize.js +4 -3
  29. package/esm/src/guardrails/sensitive.d.ts +2 -2
  30. package/esm/src/guardrails/sensitive.js +1 -1
  31. package/esm/src/host/client-turn.js +8 -1
  32. package/esm/src/interface/profile-interface.js +13 -0
  33. package/esm/src/kernel/answer-tool.d.ts +14 -0
  34. package/esm/src/kernel/answer-tool.js +26 -0
  35. package/esm/src/kernel/engine/decision-references.d.ts +14 -0
  36. package/esm/src/kernel/engine/decision-references.js +55 -0
  37. package/esm/src/kernel/engine/decision.d.ts +3 -3
  38. package/esm/src/kernel/engine/decision.js +38 -22
  39. package/esm/src/kernel/engine/embedding.js +0 -2
  40. package/esm/src/kernel/engine/runner/gates.js +40 -5
  41. package/esm/src/kernel/engine/runner/mod.js +13 -5
  42. package/esm/src/kernel/engine/runner/state.d.ts +9 -1
  43. package/esm/src/kernel/engine/runner/state.js +10 -1
  44. package/esm/src/kernel/engine/runner/steps.js +50 -13
  45. package/esm/src/kernel/engine/runner/stream.d.ts +14 -8
  46. package/esm/src/kernel/engine/runner/stream.js +25 -13
  47. package/esm/src/kernel/engine/turn-trace.js +2 -2
  48. package/esm/src/kernel/mod.d.ts +1 -1
  49. package/esm/src/kernel/mod.js +1 -1
  50. package/esm/src/kernel/profile-presence.js +12 -0
  51. package/esm/src/kernel/profile-scope.js +14 -14
  52. package/esm/src/kernel/protocols/binding.d.ts +3 -1
  53. package/esm/src/kernel/protocols/binding.js +6 -0
  54. package/esm/src/kernel/protocols/limits.d.ts +10 -11
  55. package/esm/src/kernel/protocols/limits.js +9 -23
  56. package/esm/src/kernel/protocols/runtime.d.ts +3 -2
  57. package/esm/src/kernel/protocols/runtime.js +3 -4
  58. package/esm/src/kernel/provider-budget.d.ts +2 -2
  59. package/esm/src/kernel/provider-budget.js +22 -12
  60. package/esm/src/kernel/provider-runtime.js +8 -5
  61. package/esm/src/kernel/registry/profiles.js +3 -3
  62. package/esm/src/kernel/registry/resolve.js +4 -0
  63. package/esm/src/kernel/registry/schemas.js +1 -1
  64. package/esm/src/kernel/schema.js +28 -2
  65. package/esm/src/kernel/tools/execute.js +3 -1
  66. package/esm/src/kernel/tools/image-fetch.d.ts +2 -4
  67. package/esm/src/kernel/tools/image-fetch.js +19 -20
  68. package/esm/src/kernel/tools/mcp-sessions.d.ts +7 -2
  69. package/esm/src/kernel/tools/mcp-sessions.js +21 -12
  70. package/esm/src/kernel/tools/registry.js +21 -0
  71. package/esm/src/kernel/tools/remote.d.ts +0 -2
  72. package/esm/src/kernel/tools/remote.js +28 -23
  73. package/esm/src/kernel/tools/types.d.ts +30 -4
  74. package/esm/src/kernel/turn-events.d.ts +2 -0
  75. package/esm/src/kernel/turn-events.js +1 -0
  76. package/esm/src/kernel/types.d.ts +4 -2
  77. package/esm/src/observability/spans.js +7 -8
  78. package/esm/src/observability/trace-catalog.js +1 -0
  79. package/esm/src/providers/embedding-limits.js +4 -4
  80. package/esm/src/providers/gemini/{embedding-protocol.d.ts → embeddings-protocol.d.ts} +3 -3
  81. package/esm/src/providers/gemini/{embedding-protocol.js → embeddings-protocol.js} +3 -3
  82. package/esm/src/providers/gemini/live-protocol.js +4 -1
  83. package/esm/src/providers/gemini/provider.js +2 -2
  84. package/esm/src/providers/local/embeddings-protocol.d.ts +7 -0
  85. package/esm/src/providers/local/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  86. package/esm/src/providers/local/provider.js +2 -2
  87. package/esm/src/providers/mod.d.ts +3 -3
  88. package/esm/src/providers/mod.js +3 -3
  89. package/esm/src/providers/openrouter/decisions-protocol.js +11 -8
  90. package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
  91. package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  92. package/esm/src/providers/openrouter/provider.js +2 -2
  93. package/package.json +1 -1
  94. package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
  95. package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
@@ -1,19 +1,19 @@
1
1
  // invariant: The one place a match becomes an action. Every boundary calls `detectAt`; nothing
2
2
  // else reads the matrix to decide what a match does.
3
3
  import { applySpans, spansFromPatterns } from '../observability/spans.js';
4
- import { INBOUND_BOUNDARIES, recordOf, TOOL_BOUNDARIES } from './boundaries.js';
4
+ import { recordOf, TOOL_BOUNDARIES } from './boundaries.js';
5
5
  import { canaryLeakRanges } from './canary.js';
6
6
  import { actionAt, DETECTORS, detects, } from './detectors.js';
7
7
  import { boundaryNote } from './egress.js';
8
8
  import { notePattern, SYSTEM_BOUNDARY } from './egress-patterns.js';
9
9
  import { imageLeakSpans, linkLeakSpans } from './egress-urls.js';
10
+ import { describeError } from './error.js';
10
11
  import { CANARY_HIT, hitFromSpan, promptEchoHit } from './hits.js';
11
12
  import { injectionSpans } from './injection.js';
12
13
  import { promptEchoRanges } from './prompt-echo.js';
13
14
  import { DETECT_RULES, detectRule } from './rules.js';
14
15
  import { sensitiveSpans } from './sensitive.js';
15
16
  import { SENSITIVE_GROUPS } from './sensitive-groups.js';
16
- import { TheoremError } from './theorem-error.js';
17
17
  import { directives, mergedRanges } from './tool-directives.js';
18
18
  import { ownToolsWithout, toolLeakSpans } from './tool-leak.js';
19
19
  const STRENGTH = ['allow', 'flag', 'redact', 'block'];
@@ -143,18 +143,23 @@ function leavesImages(detector, boundary, detect) {
143
143
  function foundBy(find, text, boundary) {
144
144
  try {
145
145
  const spans = find(text, boundary ? { boundary } : {});
146
+ const outside = {
147
+ signal: 'find_result',
148
+ // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
149
+ reason: 'find returned no list of stretches inside the text',
150
+ };
146
151
  if (!Array.isArray(spans))
147
- return 'find_result';
152
+ return outside;
148
153
  const inside = spans.every(({ start, end }) => Number.isInteger(start) &&
149
154
  Number.isInteger(end) &&
150
155
  start >= 0 &&
151
156
  start < end &&
152
157
  end <= text.length);
153
- return inside ? spans.map(({ start, end }) => ({ start, end, kind: 'host' })) : 'find_result';
158
+ return inside ? spans.map(({ start, end }) => ({ start, end, kind: 'host' })) : outside;
154
159
  }
155
- catch {
160
+ catch (err) {
156
161
  // why: A reading that failed found nothing it can vouch for, so the text does not cross.
157
- return 'find_threw';
162
+ return { signal: 'find_threw', reason: `find threw: ${describeError(err)}` };
158
163
  }
159
164
  }
160
165
  function hostReader(host, boundary, chosen) {
@@ -169,7 +174,9 @@ function hostReader(host, boundary, chosen) {
169
174
  if (!host.find)
170
175
  return matched;
171
176
  const found = foundBy(host.find, text, boundary);
172
- return typeof found === 'string' ? found : [...matched, ...found];
177
+ if (!Array.isArray(found))
178
+ return { ...found, reason: `Host detector '${host.id}' ${found.reason}` };
179
+ return [...matched, ...found];
173
180
  },
174
181
  hit: (text, span) => ({ ...hitFromSpan(text, span, rule, 'high'), label: host.label }),
175
182
  };
@@ -208,32 +215,17 @@ function failedHit({ rule, label }, signal) {
208
215
  ...(signal === undefined ? {} : { signal }),
209
216
  };
210
217
  }
211
- /**
212
- * The longest single text the person or host may send, in characters: far over any model's
213
- * context, and under what the detectors read safely.
214
- */
215
- const MAX_INBOUND_CHARS = 1_000_000;
216
- const INBOUND = new Set(INBOUND_BOUNDARIES);
217
- /** Refuses a text the person or host sent that is over `MAX_INBOUND_CHARS`; `where` names it for the log. */
218
- function refuseOverlong(text, where) {
219
- if (text.length <= MAX_INBOUND_CHARS)
220
- return;
221
- // labels-exempt: internal diagnostic; the person reads `input.too_long`
222
- throw new TheoremError('input', `${where} text of ${text.length} characters is over ${MAX_INBOUND_CHARS}`, {
223
- copy: { key: 'input.too_long', params: { maxChars: MAX_INBOUND_CHARS } },
224
- });
225
- }
226
218
  /** Reads `text` as it crosses `boundary` under the profile's resolved matrix. */
227
219
  function detectAt(text, boundary, detect, scope = NO_SCOPE) {
228
- if (INBOUND.has(boundary))
229
- refuseOverlong(text, boundary);
230
220
  const hits = [];
231
221
  const redact = [];
222
+ const reasons = [];
232
223
  let action = 'allow';
233
224
  for (const reader of readersAt(boundary, detect, scope)) {
234
225
  const spans = reader.spans(text);
235
- if (typeof spans === 'string') {
236
- hits.push(failedHit(reader, spans));
226
+ if (!Array.isArray(spans)) {
227
+ hits.push(failedHit(reader, spans.signal));
228
+ reasons.push(spans.reason);
237
229
  action = 'block';
238
230
  continue;
239
231
  }
@@ -242,11 +234,13 @@ function detectAt(text, boundary, detect, scope = NO_SCOPE) {
242
234
  for (const span of spans)
243
235
  hits.push(reader.hit(text, span));
244
236
  if (reader.chosen === 'redact')
245
- redact.push(...spans);
237
+ for (const span of spans)
238
+ redact.push(span);
246
239
  action = stronger(action, reader.chosen);
247
240
  }
248
- if (action === 'block')
249
- return { action, hits };
241
+ if (action === 'block') {
242
+ return { action, hits, ...(reasons.length ? { errorInternal: reasons.join('\n') } : {}) };
243
+ }
250
244
  if (redact.length === 0)
251
245
  return { action, text, hits };
252
246
  const replaced = applySpans(text, redact);
@@ -264,7 +258,7 @@ function leftAfterRedact(replaced, boundary, detect, scope, detectors) {
264
258
  if (chosen === 'flag')
265
259
  return false;
266
260
  const left = spans(replaced);
267
- return !left || left.length > 0;
261
+ return !Array.isArray(left) || left.length > 0;
268
262
  });
269
263
  }
270
264
  /** The detectors that read `boundary`: every one not set to `ignore`. */
@@ -298,16 +292,21 @@ function detectReads(boundaries, detect, scope) {
298
292
  */
299
293
  function detectRelease(window, { from, to, settled }, boundary, detect, { detectors, scope }) {
300
294
  const failed = [];
295
+ const reasons = [];
301
296
  const found = readersAt(boundary, detect, scope, detectors).flatMap((reader) => {
302
297
  const spans = reader.spans(window);
303
- if (typeof spans === 'string')
304
- failed.push(failedHit(reader, spans));
305
- return (typeof spans === 'string' ? [] : spans)
298
+ if (!Array.isArray(spans)) {
299
+ failed.push(failedHit(reader, spans.signal));
300
+ reasons.push(spans.reason);
301
+ return [];
302
+ }
303
+ return spans
306
304
  .filter((span) => span.end > from && span.start < to)
307
305
  .map((span) => ({ span, rule: reader.rule, label: reader.label, chosen: reader.chosen }));
308
306
  });
309
- if (failed.length > 0)
310
- return { action: 'block', hits: failed, taken: 0 };
307
+ if (failed.length > 0) {
308
+ return { action: 'block', hits: failed, taken: 0, errorInternal: reasons.join('\n') };
309
+ }
311
310
  const hitOf = ({ span, rule, label }) => ({
312
311
  ...hitFromSpan(window, span, rule, 'high'),
313
312
  ...(label === undefined ? {} : { label }),
@@ -370,23 +369,31 @@ function storedHostSpans(text, detect) {
370
369
  if (!host.find || !text)
371
370
  return matched;
372
371
  const found = foundBy(host.find, text);
373
- return typeof found === 'string'
374
- ? [{ start: 0, end: text.length, kind: 'host' }]
375
- : [...matched, ...found];
372
+ return Array.isArray(found)
373
+ ? [...matched, ...found]
374
+ : [{ start: 0, end: text.length, kind: 'host' }];
376
375
  });
377
376
  }
378
377
  /** A reader for everything crossing `boundary` in one request. */
379
378
  function boundaryReader(boundary, detect) {
380
379
  const hits = [];
380
+ const reasons = [];
381
381
  let action = 'allow';
382
382
  return {
383
383
  read(text) {
384
384
  const detected = detectAt(text, boundary, detect);
385
- hits.push(...detected.hits);
385
+ for (const hit of detected.hits)
386
+ hits.push(hit);
387
+ if (detected.errorInternal)
388
+ reasons.push(detected.errorInternal);
386
389
  action = stronger(action, detected.action);
387
390
  return detected.text ?? '';
388
391
  },
389
- found: () => ({ action, hits }),
392
+ found: () => ({
393
+ action,
394
+ hits,
395
+ ...(reasons.length ? { errorInternal: reasons.join('\n') } : {}),
396
+ }),
390
397
  };
391
398
  }
392
399
  const TOOL_STAGE = Object.fromEntries(TOOL_BOUNDARIES.map((boundary) => [
@@ -429,6 +436,9 @@ function detectEvent(boundary, found, provenance, stage = BOUNDARY_STAGE[boundar
429
436
  action: found.action,
430
437
  hits: found.hits,
431
438
  ...(provenance ? { provenance } : {}),
439
+ ...(found.action === 'block' && found.errorInternal
440
+ ? { errorInternal: found.errorInternal }
441
+ : {}),
432
442
  };
433
443
  }
434
- export { boundaryReader, detectAt, detectEvent, detectorsAt, detectReads, detectRelease, hostAt, isScoped, leakScopeOf, redactDetectors, refuseOverlong, scopeOf, storedHostSpans, storedSpans, stronger, };
444
+ export { boundaryReader, detectAt, detectEvent, detectorsAt, detectReads, detectRelease, hostAt, isScoped, leakScopeOf, redactDetectors, scopeOf, storedHostSpans, storedSpans, stronger, };
@@ -26,7 +26,7 @@ interface ReplyRead {
26
26
  blocked?: GuardrailHit[];
27
27
  }
28
28
  /** A JSON value read at `boundary`, with each match in a string replaced, or what stops it crossing. */
29
- declare function readValue(value: unknown, boundary: Boundary, detect: ResolvedDetect, scope?: DetectScope): Pick<Detection, 'action' | 'hits'> & {
29
+ declare function readValue(value: unknown, boundary: Boundary, detect: ResolvedDetect, scope?: DetectScope): Pick<Detection, 'action' | 'hits' | 'errorInternal'> & {
30
30
  value?: unknown;
31
31
  };
32
32
  /**
@@ -23,8 +23,10 @@ function readValue(value, boundary, detect, scope = {}) {
23
23
  return { action: 'block', hits: [{ rule: EGRESS_RULES.unscannable, severity: 'high' }] };
24
24
  }
25
25
  const found = detectAt(scan.text, boundary, detect, scope);
26
- if (found.action !== 'redact')
27
- return { action: found.action, hits: found.hits, value };
26
+ if (found.action !== 'redact') {
27
+ const why = found.errorInternal ? { errorInternal: found.errorInternal } : {};
28
+ return { action: found.action, hits: found.hits, value, ...why };
29
+ }
28
30
  const redacted = mapStrings(value, (leaf) => detectAt(leaf, boundary, detect, scope).text ?? '');
29
31
  const left = detectAt(textForScan(redacted).text, boundary, detect, scope).action;
30
32
  // why: A match in a key, or one that runs across values, has no string to replace: it does not cross.