@theoremjs/agents 0.3.4-next.522.g9e684cd → 0.3.4-next.528.g44c76f9

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.
@@ -15,6 +15,8 @@ interface Detection {
15
15
  hits: GuardrailHit[];
16
16
  /** Why a host's `find` failed, for the builder only: it reaches `guardrail.errorInternal`. */
17
17
  errorInternal?: string;
18
+ /** The same failure in the kernel's words only, with no text from the host: what the trace stores. */
19
+ errorSummary?: string;
18
20
  }
19
21
  declare function stronger(left: DetectOutcome, right: DetectOutcome | 'ignore'): DetectOutcome;
20
22
  /**
@@ -94,7 +96,7 @@ declare function storedHostSpans(text: string, detect?: ResolvedDetect): RedactS
94
96
  interface BoundaryReader {
95
97
  /** The text to let through, or `''` once a match blocks: the caller reads `found().action`. */
96
98
  read(text: string): string;
97
- found(): Pick<Detection, 'action' | 'hits' | 'errorInternal'>;
99
+ found(): Pick<Detection, 'action' | 'hits' | 'errorInternal' | 'errorSummary'>;
98
100
  }
99
101
  /** A reader for everything crossing `boundary` in one request. */
100
102
  declare function boundaryReader(boundary: Boundary, detect: ResolvedDetect): BoundaryReader;
@@ -102,6 +104,6 @@ declare function boundaryReader(boundary: Boundary, detect: ResolvedDetect): Bou
102
104
  * The guardrail event for what was found at a boundary, or `undefined` when nothing matched.
103
105
  * `stage` is the boundary's own unless the text was read as it streamed.
104
106
  */
105
- declare function detectEvent(boundary: Boundary, found: Pick<Detection, 'action' | 'hits' | 'errorInternal'>, provenance?: Provenance, stage?: GuardrailStage): GuardrailEvent | undefined;
107
+ declare function detectEvent(boundary: Boundary, found: Pick<Detection, 'action' | 'hits' | 'errorInternal' | 'errorSummary'>, provenance?: Provenance, stage?: GuardrailStage): GuardrailEvent | undefined;
106
108
  export type { BoundaryReader, Detection, DetectOutcome, DetectScope, Release, StoredSides, StreamRead, Stretch, };
107
109
  export { boundaryReader, detectAt, detectEvent, detectorsAt, detectReads, detectRelease, hostAt, isScoped, leakScopeOf, redactDetectors, scopeOf, storedHostSpans, storedSpans, stronger, };
@@ -16,6 +16,14 @@ import { sensitiveSpans } from './sensitive.js';
16
16
  import { SENSITIVE_GROUPS } from './sensitive-groups.js';
17
17
  import { directives, mergedRanges } from './tool-directives.js';
18
18
  import { ownToolsWithout, toolLeakSpans } from './tool-leak.js';
19
+ function failureText(failures) {
20
+ if (failures.length === 0)
21
+ return {};
22
+ return {
23
+ errorInternal: failures.map(({ errorInternal }) => errorInternal).join('\n'),
24
+ errorSummary: failures.map(({ errorSummary }) => errorSummary).join('\n'),
25
+ };
26
+ }
19
27
  const STRENGTH = ['allow', 'flag', 'redact', 'block'];
20
28
  function stronger(left, right) {
21
29
  if (right === 'ignore')
@@ -139,15 +147,18 @@ function hitOf(detector, text, span) {
139
147
  function leavesImages(detector, boundary, detect) {
140
148
  return detector === 'ungiven_links' && detect.ungiven_images[boundary] !== 'ignore';
141
149
  }
150
+ /** A thrown value's type: a name of letters only, else `Error`, so no host text gets through. */
151
+ function thrownType(err) {
152
+ const name = err instanceof Error ? err.name : typeof err;
153
+ return /^[A-Za-z]{1,64}$/.test(name) ? name : 'Error';
154
+ }
142
155
  /** What a host's `find` matched in `text`, or how it failed. */
143
156
  function foundBy(find, text, boundary) {
144
157
  try {
145
158
  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
- };
159
+ // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
160
+ const said = 'find returned no list of stretches inside the text';
161
+ const outside = { signal: 'find_result', reason: said, summary: said };
151
162
  if (!Array.isArray(spans))
152
163
  return outside;
153
164
  const inside = spans.every(({ start, end }) => Number.isInteger(start) &&
@@ -159,7 +170,12 @@ function foundBy(find, text, boundary) {
159
170
  }
160
171
  catch (err) {
161
172
  // why: A reading that failed found nothing it can vouch for, so the text does not cross.
162
- return { signal: 'find_threw', reason: `find threw: ${describeError(err)}` };
173
+ const id = crypto.randomUUID().slice(0, 8);
174
+ return {
175
+ signal: 'find_threw',
176
+ reason: `find threw: ${describeError(err)} (error ${id})`,
177
+ summary: `find threw ${thrownType(err)} (error ${id})`,
178
+ };
163
179
  }
164
180
  }
165
181
  function hostReader(host, boundary, chosen) {
@@ -174,8 +190,10 @@ function hostReader(host, boundary, chosen) {
174
190
  if (!host.find)
175
191
  return matched;
176
192
  const found = foundBy(host.find, text, boundary);
177
- if (!Array.isArray(found))
178
- return { ...found, reason: `Host detector '${host.id}' ${found.reason}` };
193
+ if (!Array.isArray(found)) {
194
+ const by = `Host detector '${host.id}'`;
195
+ return { ...found, reason: `${by} ${found.reason}`, summary: `${by} ${found.summary}` };
196
+ }
179
197
  return [...matched, ...found];
180
198
  },
181
199
  hit: (text, span) => ({ ...hitFromSpan(text, span, rule, 'high'), label: host.label }),
@@ -219,13 +237,13 @@ function failedHit({ rule, label }, signal) {
219
237
  function detectAt(text, boundary, detect, scope = NO_SCOPE) {
220
238
  const hits = [];
221
239
  const redact = [];
222
- const reasons = [];
240
+ const failures = [];
223
241
  let action = 'allow';
224
242
  for (const reader of readersAt(boundary, detect, scope)) {
225
243
  const spans = reader.spans(text);
226
244
  if (!Array.isArray(spans)) {
227
245
  hits.push(failedHit(reader, spans.signal));
228
- reasons.push(spans.reason);
246
+ failures.push({ errorInternal: spans.reason, errorSummary: spans.summary });
229
247
  action = 'block';
230
248
  continue;
231
249
  }
@@ -239,7 +257,7 @@ function detectAt(text, boundary, detect, scope = NO_SCOPE) {
239
257
  action = stronger(action, reader.chosen);
240
258
  }
241
259
  if (action === 'block') {
242
- return { action, hits, ...(reasons.length ? { errorInternal: reasons.join('\n') } : {}) };
260
+ return { action, hits, ...failureText(failures) };
243
261
  }
244
262
  if (redact.length === 0)
245
263
  return { action, text, hits };
@@ -292,12 +310,12 @@ function detectReads(boundaries, detect, scope) {
292
310
  */
293
311
  function detectRelease(window, { from, to, settled }, boundary, detect, { detectors, scope }) {
294
312
  const failed = [];
295
- const reasons = [];
313
+ const failures = [];
296
314
  const found = readersAt(boundary, detect, scope, detectors).flatMap((reader) => {
297
315
  const spans = reader.spans(window);
298
316
  if (!Array.isArray(spans)) {
299
317
  failed.push(failedHit(reader, spans.signal));
300
- reasons.push(spans.reason);
318
+ failures.push({ errorInternal: spans.reason, errorSummary: spans.summary });
301
319
  return [];
302
320
  }
303
321
  return spans
@@ -305,7 +323,7 @@ function detectRelease(window, { from, to, settled }, boundary, detect, { detect
305
323
  .map((span) => ({ span, rule: reader.rule, label: reader.label, chosen: reader.chosen }));
306
324
  });
307
325
  if (failed.length > 0) {
308
- return { action: 'block', hits: failed, taken: 0, errorInternal: reasons.join('\n') };
326
+ return { action: 'block', hits: failed, taken: 0, ...failureText(failures) };
309
327
  }
310
328
  const hitOf = ({ span, rule, label }) => ({
311
329
  ...hitFromSpan(window, span, rule, 'high'),
@@ -377,23 +395,23 @@ function storedHostSpans(text, detect) {
377
395
  /** A reader for everything crossing `boundary` in one request. */
378
396
  function boundaryReader(boundary, detect) {
379
397
  const hits = [];
380
- const reasons = [];
398
+ const failures = [];
381
399
  let action = 'allow';
382
400
  return {
383
401
  read(text) {
384
402
  const detected = detectAt(text, boundary, detect);
385
403
  for (const hit of detected.hits)
386
404
  hits.push(hit);
387
- if (detected.errorInternal)
388
- reasons.push(detected.errorInternal);
405
+ if (detected.errorInternal && detected.errorSummary) {
406
+ failures.push({
407
+ errorInternal: detected.errorInternal,
408
+ errorSummary: detected.errorSummary,
409
+ });
410
+ }
389
411
  action = stronger(action, detected.action);
390
412
  return detected.text ?? '';
391
413
  },
392
- found: () => ({
393
- action,
394
- hits,
395
- ...(reasons.length ? { errorInternal: reasons.join('\n') } : {}),
396
- }),
414
+ found: () => ({ action, hits, ...failureText(failures) }),
397
415
  };
398
416
  }
399
417
  const TOOL_STAGE = Object.fromEntries(TOOL_BOUNDARIES.map((boundary) => [
@@ -439,6 +457,7 @@ function detectEvent(boundary, found, provenance, stage = BOUNDARY_STAGE[boundar
439
457
  ...(found.action === 'block' && found.errorInternal
440
458
  ? { errorInternal: found.errorInternal }
441
459
  : {}),
460
+ ...(found.action === 'block' && found.errorSummary ? { errorSummary: found.errorSummary } : {}),
442
461
  };
443
462
  }
444
463
  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' | 'errorInternal'> & {
29
+ declare function readValue(value: unknown, boundary: Boundary, detect: ResolvedDetect, scope?: DetectScope): Pick<Detection, 'action' | 'hits' | 'errorInternal' | 'errorSummary'> & {
30
30
  value?: unknown;
31
31
  };
32
32
  /**
@@ -24,7 +24,8 @@ function readValue(value, boundary, detect, scope = {}) {
24
24
  }
25
25
  const found = detectAt(scan.text, boundary, detect, scope);
26
26
  if (found.action !== 'redact') {
27
- const why = found.errorInternal ? { errorInternal: found.errorInternal } : {};
27
+ const { errorInternal, errorSummary } = found;
28
+ const why = errorInternal && errorSummary ? { errorInternal, errorSummary } : {};
28
29
  return { action: found.action, hits: found.hits, value, ...why };
29
30
  }
30
31
  const redacted = mapStrings(value, (leaf) => detectAt(leaf, boundary, detect, scope).text ?? '');
@@ -18,6 +18,10 @@ declare function errorCopyText(copy: ErrorCopies, labels?: LabelOverrides): stri
18
18
  declare function publicError(err: unknown, labels?: LabelOverrides): string;
19
19
  /** Raw diagnostic for hosts, traces, and logs; never shown to end users. Each cause that says something new follows on its own line. */
20
20
  declare function describeError(err: unknown): string;
21
+ /** fetch rejects only when no response came back, so any non-abort, non-timeout rejection is `network`. */
22
+ declare function networkError(err: unknown): unknown;
23
+ /** `send`, with a failed send or body read named as `networkError` names it. */
24
+ declare function sendOrNetworkError(send: typeof fetch, ...args: Parameters<typeof fetch>): Promise<Response>;
21
25
  type ProducedError = TurnEventOf<'error'> & {
22
26
  errorInternal: string;
23
27
  };
@@ -28,4 +32,4 @@ declare function withPublicLabels(event: TurnEvent, labels?: LabelOverrides): Tu
28
32
  export type { ErrorCopies, ErrorCopy, ErrorKind, TheoremErrorOptions } from './theorem-error.js';
29
33
  export { ERROR_KINDS } from './theorem-error.js';
30
34
  export type { ProducedError };
31
- export { describeError, errorCopyText, errorKind, isAbortError, isTimeoutError, kindOfHttpStatus, publicError, TheoremError, throwIfAborted, toErrorEvent, withPublicLabels, };
35
+ export { describeError, errorCopyText, errorKind, isAbortError, isTimeoutError, kindOfHttpStatus, networkError, publicError, sendOrNetworkError, TheoremError, throwIfAborted, toErrorEvent, withPublicLabels, };
@@ -88,6 +88,49 @@ function describeError(err) {
88
88
  }
89
89
  return lines.join('\n');
90
90
  }
91
+ /** fetch rejects only when no response came back, so any non-abort, non-timeout rejection is `network`. */
92
+ function networkError(err) {
93
+ if (err instanceof TheoremError || isAbortError(err) || isTimeoutError(err)) {
94
+ return err;
95
+ }
96
+ return new TheoremError('network', describeError(err), { cause: err });
97
+ }
98
+ /** `response` with a body whose read that fails is named as `networkError` names it. */
99
+ function bodyOrNetworkError(response) {
100
+ if (!response.body)
101
+ return response;
102
+ const reader = response.body.getReader();
103
+ const body = new ReadableStream({
104
+ async pull(controller) {
105
+ try {
106
+ const next = await reader.read();
107
+ if (next.done)
108
+ controller.close();
109
+ else
110
+ controller.enqueue(next.value);
111
+ }
112
+ catch (err) {
113
+ controller.error(networkError(err));
114
+ }
115
+ },
116
+ cancel: (reason) => reader.cancel(reason),
117
+ }, { highWaterMark: 0 });
118
+ const named = new Response(body, response);
119
+ Object.defineProperties(named, {
120
+ url: { value: response.url },
121
+ redirected: { value: response.redirected },
122
+ });
123
+ return named;
124
+ }
125
+ /** `send`, with a failed send or body read named as `networkError` names it. */
126
+ async function sendOrNetworkError(send, ...args) {
127
+ try {
128
+ return bodyOrNetworkError(await send(...args));
129
+ }
130
+ catch (err) {
131
+ throw networkError(err);
132
+ }
133
+ }
91
134
  /** No user wording yet: `withPublicLabels` adds it where the event reaches the host, the one place that knows the profile. */
92
135
  function toErrorEvent(err) {
93
136
  return {
@@ -112,4 +155,4 @@ function withPublicLabels(event, labels) {
112
155
  return event;
113
156
  }
114
157
  export { ERROR_KINDS } from './theorem-error.js';
115
- export { describeError, errorCopyText, errorKind, isAbortError, isTimeoutError, kindOfHttpStatus, publicError, TheoremError, throwIfAborted, toErrorEvent, withPublicLabels, };
158
+ export { describeError, errorCopyText, errorKind, isAbortError, isTimeoutError, kindOfHttpStatus, networkError, publicError, sendOrNetworkError, TheoremError, throwIfAborted, toErrorEvent, withPublicLabels, };
@@ -95,6 +95,11 @@ export interface GuardrailEvent {
95
95
  provenance?: Provenance;
96
96
  /** The block verdict's `errorInternal`: builder-only, stripped by `forClient`. */
97
97
  errorInternal?: string;
98
+ /**
99
+ * A failed host detector's `errorInternal` in the kernel's words only, with the same error id:
100
+ * what the trace stores. Builder-only, stripped by `forClient`.
101
+ */
102
+ errorSummary?: string;
98
103
  }
99
104
  /** The schema a guardrail event parses against. */
100
105
  export declare const guardrailEventSchema: z.ZodType<GuardrailEvent>;
@@ -42,6 +42,7 @@ const guardrailEvent = z.object({
42
42
  hits: z.array(guardrailHit),
43
43
  provenance: provenance.optional(),
44
44
  errorInternal: z.string().optional(),
45
+ errorSummary: z.string().optional(),
45
46
  });
46
47
  true;
47
48
  /** The schema a guardrail event parses against. */
@@ -1,4 +1,4 @@
1
- import { TheoremError } from './error.js';
1
+ import { sendOrNetworkError, TheoremError } from './error.js';
2
2
  /**
3
3
  * IPv4 ranges refused outright:
4
4
  * - Loopback: 127.0.0.0/8
@@ -265,7 +265,7 @@ export async function fetchGuarded(url, init, options) {
265
265
  sent.set(name, value);
266
266
  }
267
267
  }
268
- const response = await fetchFn(target.href, {
268
+ const response = await sendOrNetworkError(fetchFn, target.href, {
269
269
  ...init,
270
270
  method,
271
271
  headers: sent,
@@ -305,7 +305,7 @@ export function dnsOverHttpsResolver(options) {
305
305
  const url = new URL(options.endpoint);
306
306
  url.searchParams.set('name', hostname);
307
307
  url.searchParams.set('type', String(type));
308
- const response = await fetchFn(url.href, {
308
+ const response = await sendOrNetworkError(fetchFn, url.href, {
309
309
  headers: { accept: 'application/dns-json' },
310
310
  ...(signal ? { signal } : {}),
311
311
  });
@@ -41,10 +41,11 @@ function stripCompactionInternal(event) {
41
41
  return { ...event, failure };
42
42
  }
43
43
  function stripGuardrailInternal(event) {
44
- if (event.type !== 'guardrail' || !event.guardrail?.errorInternal) {
44
+ if (event.type !== 'guardrail' ||
45
+ !(event.guardrail?.errorInternal || event.guardrail?.errorSummary)) {
45
46
  return event;
46
47
  }
47
- const { errorInternal: _internal, ...guardrail } = event.guardrail;
48
+ const { errorInternal: _internal, errorSummary: _summary, ...guardrail } = event.guardrail;
48
49
  return { ...event, guardrail };
49
50
  }
50
51
  function stripEvidenceRaw(event) {
@@ -5,16 +5,26 @@ import type { KernelRegistry } from '../registry/kernel-registry.js';
5
5
  import type { DecisionProfile, DecisionRequest, DecisionResult } from '../types.js';
6
6
  /** Each decision failure code, and the error kind it reads as. */
7
7
  declare const DECISION_ERROR_KINDS: {
8
+ readonly invalid_config: "config";
8
9
  readonly invalid_request: "request";
10
+ readonly invalid_input: "input";
11
+ readonly not_allowed: "action";
9
12
  readonly authentication: "auth";
10
13
  readonly permission: "auth";
11
14
  readonly rate_limited: "rate_limit";
15
+ readonly busy: "busy";
16
+ readonly unsupported: "unsupported";
12
17
  readonly unavailable: "unavailable";
13
18
  readonly network: "network";
14
19
  readonly timeout: "timeout";
15
20
  readonly cancelled: "cancelled";
16
21
  readonly malformed_response: "bad_response";
22
+ readonly refused: "safety";
23
+ readonly blocked: "blocked";
17
24
  readonly disclosure_blocked: "blocked";
25
+ readonly declined: "declined";
26
+ readonly failed: "failed";
27
+ readonly internal: "internal";
18
28
  };
19
29
  /**
20
30
  * A normalized native-decision failure; response bodies are deliberately omitted. What the
@@ -1,4 +1,4 @@
1
- import { TheoremError, throwIfAborted } from '../../guardrails/error.js';
1
+ import { errorKind, TheoremError, throwIfAborted } from '../../guardrails/error.js';
2
2
  import { labelText } from '../../guardrails/labels.js';
3
3
  import { traceJson } from '../../observability/trace-span.js';
4
4
  import { guardedCall } from '../provider-runtime.js';
@@ -8,16 +8,47 @@ import { tracedCall } from './call-trace.js';
8
8
  import { referencedAssets } from './decision-references.js';
9
9
  /** Each decision failure code, and the error kind it reads as. */
10
10
  const DECISION_ERROR_KINDS = {
11
+ invalid_config: 'config',
11
12
  invalid_request: 'request',
13
+ invalid_input: 'input',
14
+ not_allowed: 'action',
12
15
  authentication: 'auth',
13
16
  permission: 'auth',
14
17
  rate_limited: 'rate_limit',
18
+ busy: 'busy',
19
+ unsupported: 'unsupported',
15
20
  unavailable: 'unavailable',
16
21
  network: 'network',
17
22
  timeout: 'timeout',
18
23
  cancelled: 'cancelled',
19
24
  malformed_response: 'bad_response',
25
+ refused: 'safety',
26
+ blocked: 'blocked',
20
27
  disclosure_blocked: 'blocked',
28
+ declined: 'declined',
29
+ failed: 'failed',
30
+ internal: 'internal',
31
+ };
32
+ /** Each kind's code: a thrown failure keeps its kind. */
33
+ const DECISION_CODES = {
34
+ config: 'invalid_config',
35
+ request: 'invalid_request',
36
+ input: 'invalid_input',
37
+ action: 'not_allowed',
38
+ auth: 'authentication',
39
+ rate_limit: 'rate_limited',
40
+ busy: 'busy',
41
+ unsupported: 'unsupported',
42
+ unavailable: 'unavailable',
43
+ bad_response: 'malformed_response',
44
+ network: 'network',
45
+ timeout: 'timeout',
46
+ safety: 'refused',
47
+ blocked: 'blocked',
48
+ declined: 'declined',
49
+ failed: 'failed',
50
+ cancelled: 'cancelled',
51
+ internal: 'internal',
21
52
  };
22
53
  /**
23
54
  * A normalized native-decision failure; response bodies are deliberately omitted. What the
@@ -363,25 +394,10 @@ function recordResult(root, result) {
363
394
  function decisionProviderError(error) {
364
395
  if (error instanceof DecisionError)
365
396
  return error;
366
- const kind = error instanceof TheoremError
367
- ? error.kind
368
- : error instanceof TypeError
369
- ? 'network'
370
- : 'bad_response';
371
- const codes = {
372
- auth: 'authentication',
373
- rate_limit: 'rate_limited',
374
- network: 'network',
375
- busy: 'unavailable',
376
- unavailable: 'unavailable',
377
- timeout: 'timeout',
378
- cancelled: 'cancelled',
379
- request: 'invalid_request',
380
- unsupported: 'invalid_request',
381
- };
397
+ const kind = errorKind(error);
382
398
  const code = kind === 'auth' && isRecord(error) && error.status === 403
383
399
  ? 'permission'
384
- : (codes[kind] ?? 'malformed_response');
400
+ : DECISION_CODES[kind];
385
401
  return new DecisionError(code,
386
402
  // why: A framework failure's message holds no response body, and says what stopped the decision.
387
403
  error instanceof TheoremError ? error.message : labelText('provider.decision_failed'), isRecord(error) && typeof error.status === 'number' ? error.status : undefined, { cause: error });
@@ -278,6 +278,7 @@ function guardrailAttributes(guardrail) {
278
278
  hits: guardrail.hits.map((hit) => ({
279
279
  rule: hit.rule,
280
280
  severity: hit.severity,
281
+ ...optional('signal', hit.signal),
281
282
  ...(hit.span ? { start: hit.span.start, end: hit.span.end } : {}),
282
283
  ...optional('match', hit.match),
283
284
  ...optional('label', hit.label),
@@ -285,6 +286,7 @@ function guardrailAttributes(guardrail) {
285
286
  })),
286
287
  ...(guardrail.provenance ? { provenance: { ...guardrail.provenance } } : {}),
287
288
  ...(guardrail.errorInternal ? { error: traceContent(guardrail.errorInternal) } : {}),
289
+ ...optional('error_summary', guardrail.errorSummary),
288
290
  };
289
291
  }
290
292
  /** What a stream check looked at, for its pass record. */
@@ -1,7 +1,7 @@
1
1
  import { TheoremError, throwIfAborted } from '../../guardrails/error.js';
2
2
  import { labelText } from '../../guardrails/labels.js';
3
3
  import { jsonObjectSchema } from '../json.js';
4
- import { operationUnavailable, operationWireValue, toolInvocationHash, toolOperationOutcomeSchema, } from './operation.js';
4
+ import { operationBroken, operationWireValue, toolInvocationHash, toolOperationOutcomeSchema, } from './operation.js';
5
5
  class ReplayedOperation {
6
6
  outcome;
7
7
  constructor(outcome) {
@@ -41,7 +41,7 @@ class CoordinatedExecution {
41
41
  if (result.kind === 'running' || result.kind === 'uncertain')
42
42
  throw uncertainError();
43
43
  if (result.kind !== 'acquired' || typeof result.claimToken !== 'string' || !result.claimToken)
44
- throw operationUnavailable();
44
+ throw operationBroken("The tool coordinator's begin returned no claim"); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
45
45
  this.claim = { ...operation, claimToken: result.claimToken };
46
46
  throwIfAborted(this.signal);
47
47
  return this.claim;
@@ -64,7 +64,7 @@ class CoordinatedExecution {
64
64
  return undefined;
65
65
  }
66
66
  if (!this.terminal)
67
- throw operationUnavailable();
67
+ throw operationBroken('The tool run ended with no terminal event'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
68
68
  const { modelResult, outputRaw, failure, awaiting, denied, callNotStarted, aborted, pendingInject, } = settlement;
69
69
  const effects = this.claim ? (failure ? 'unknown' : 'completed') : 'not_started';
70
70
  const outcome = {
@@ -96,7 +96,7 @@ class CoordinatedExecution {
96
96
  if (saved.event.tool.operationId !== operationId ||
97
97
  saved.event.tool.callId !== callId ||
98
98
  saved.event.tool.name !== toolId)
99
- throw operationUnavailable();
99
+ throw operationBroken("The tool coordinator's saved outcome is another operation's"); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
100
100
  this.committed = true;
101
101
  return saved;
102
102
  }
@@ -122,7 +122,7 @@ export async function* coordinateTool(coordinator, identity, invocation, signal,
122
122
  throw uncertainError();
123
123
  if (existing?.kind === 'settled') {
124
124
  if (!authorizeReplay)
125
- throw operationUnavailable();
125
+ throw operationBroken('A settled tool operation has no replay check'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
126
126
  const saved = execution.replay(new ReplayedOperation(existing.outcome));
127
127
  await authorizeReplay(saved);
128
128
  throwIfAborted(signal);
@@ -146,7 +146,7 @@ export async function* coordinateTool(coordinator, identity, invocation, signal,
146
146
  catch (error) {
147
147
  if (error instanceof ReplayedOperation) {
148
148
  if (!authorizeReplay)
149
- throw operationUnavailable();
149
+ throw operationBroken('A settled tool operation has no replay check'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
150
150
  const saved = execution.replay(error);
151
151
  await authorizeReplay(saved);
152
152
  yield saved.event;
@@ -16,7 +16,7 @@ import { dispatchToolBody, failedActivity, failedActivityOf, failureEvent, messa
16
16
  import { HandlerEvents } from './handler-events.js';
17
17
  import { fetchToolImages } from './image-fetch.js';
18
18
  import { formatToolFailureForModel, formatToolResult, frameToolFailure } from './model-text.js';
19
- import { operationUnavailable, operationWireValue } from './operation.js';
19
+ import { operationBroken, operationWireValue } from './operation.js';
20
20
  import { checkPermission, gateDetails, isGateResumeDenied, isGateResumeGranted, isResumeContinuation, } from './permission.js';
21
21
  import { executeHttpTool, executeMcpTool, failureWithoutSecret, modelResultFromOutput, omitSecret, outcomeFromUnauth, parseToolOutput, refusedCredentialOutcome, resolveToolAuth, withMedia, } from './remote.js';
22
22
  import { extractLoadedIds, profileToolAllow, profileToolsSpec, promoteLoadedTools, } from './resolve.js';
@@ -467,13 +467,14 @@ async function* receivePageAnswer(tool, ctx, claim, base) {
467
467
  const execution = ctx.pageExecution;
468
468
  if (tool.answeredBy !== 'page' || !execution)
469
469
  return ctx;
470
- if (!claim?.invocation)
471
- throw operationUnavailable();
470
+ if (!claim?.invocation) {
471
+ throw new TheoremError('config', `Page tool '${tool.name}' needs a tool execution coordinator (execution)`);
472
+ }
472
473
  const argumentsForPage = jsonObjectSchema.parse(operationWireValue(claim.invocation.preparedInput));
473
474
  const dispatch = await execution.dispatch(claim, ctx.signal);
474
475
  throwIfAborted(ctx.signal);
475
476
  if (!dispatch.grantId)
476
- throw operationUnavailable();
477
+ throw operationBroken('The page dispatch returned no grantId'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
477
478
  yield toolEvent(base, {
478
479
  phase: 'dispatch',
479
480
  grantId: dispatch.grantId,
@@ -1,4 +1,5 @@
1
- import { operationUnavailable, } from './operation.js';
1
+ import { TheoremError } from '../../guardrails/error.js';
2
+ import { operationBroken, } from './operation.js';
2
3
  /** How many operations a memory coordinator tracks before it refuses new ones. */
3
4
  const DEFAULT_MEMORY_EXECUTION_LIMIT = 256;
4
5
  /**
@@ -23,6 +24,8 @@ export function createMemoryToolExecution(options = {}) {
23
24
  const record = records.get(keyOf(claim));
24
25
  return record?.state === 'running' && record.claimToken === claim.claimToken;
25
26
  };
27
+ const full = () => new TheoremError('config', `The memory tool coordinator holds its maxOperations of ${limit}`);
28
+ const notHeld = () => operationBroken('The memory tool coordinator holds no running claim for this token'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
26
29
  return {
27
30
  lookup: (operation) => Promise.resolve(known(operation)),
28
31
  begin: (operation) => {
@@ -30,28 +33,29 @@ export function createMemoryToolExecution(options = {}) {
30
33
  if (earlier)
31
34
  return Promise.resolve(earlier);
32
35
  if (records.size >= limit)
33
- return Promise.reject(operationUnavailable());
36
+ return Promise.reject(full());
34
37
  const claimToken = crypto.randomUUID();
35
38
  records.set(keyOf(operation), { state: 'running', claimToken });
36
39
  return Promise.resolve({ kind: 'acquired', claimToken });
37
40
  },
38
41
  settle: (claim, outcome) => {
39
42
  if (!holds(claim))
40
- return Promise.reject(operationUnavailable());
43
+ return Promise.reject(notHeld());
41
44
  records.set(keyOf(claim), { state: 'settled', outcome });
42
45
  return Promise.resolve();
43
46
  },
44
47
  settleNotStarted: (operation, outcome) => {
45
- if (records.has(keyOf(operation)))
46
- return Promise.reject(operationUnavailable());
48
+ if (records.has(keyOf(operation))) {
49
+ return Promise.reject(operationBroken('The memory tool coordinator already has a record for this operation'));
50
+ }
47
51
  if (records.size >= limit)
48
- return Promise.reject(operationUnavailable());
52
+ return Promise.reject(full());
49
53
  records.set(keyOf(operation), { state: 'settled', outcome });
50
54
  return Promise.resolve();
51
55
  },
52
56
  markUncertain: (claim) => {
53
57
  if (!holds(claim))
54
- return Promise.reject(operationUnavailable());
58
+ return Promise.reject(notHeld());
55
59
  records.set(keyOf(claim), { state: 'uncertain' });
56
60
  return Promise.resolve();
57
61
  },
@@ -67,7 +67,8 @@ export interface ToolExecutionCoordinator {
67
67
  settleNotStarted(operation: ToolOperationIdentity, outcome: ToolOperationOutcome): Promise<void>;
68
68
  markUncertain(claim: ToolOperationClaim, reason: ToolOperationUncertainty): Promise<void>;
69
69
  }
70
- export declare function operationUnavailable(): TheoremError;
70
+ /** A coordinator, a page dispatch or the kernel broke the tool-operation contract. */
71
+ export declare function operationBroken(reason: string): TheoremError;
71
72
  export declare function toolInvocationHash(value: unknown): Promise<string>;
72
73
  export declare const toolOperationOutcomeSchema: z.ZodType<ToolOperationOutcome>;
73
74
  export declare function operationWireValue(value: unknown): JsonValue;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { publicError, TheoremError } from '../../guardrails/error.js';
2
+ import { TheoremError } from '../../guardrails/error.js';
3
3
  import { errorKindSchema, guardrailHit, provenance } from '../../guardrails/event-schemas.js';
4
4
  import { jsonObjectSchema, jsonValueSchema } from '../json.js';
5
5
  import { interactionPartSchema, TURN_EVENT_SCHEMAS, turnHistoryMessageSchema, } from '../turn-events.js';
@@ -7,10 +7,9 @@ export const savedToolInvocationSchema = z.strictObject({
7
7
  accepted: jsonObjectSchema,
8
8
  preparedInput: jsonValueSchema,
9
9
  });
10
- export function operationUnavailable() {
11
- const error = new TheoremError('unavailable', '');
12
- error.message = publicError(error);
13
- return error;
10
+ /** A coordinator, a page dispatch or the kernel broke the tool-operation contract. */
11
+ export function operationBroken(reason) {
12
+ return new TheoremError('internal', reason);
14
13
  }
15
14
  export async function toolInvocationHash(value) {
16
15
  const ordered = (item) => {
@@ -78,20 +77,23 @@ export function operationWireValue(value) {
78
77
  if (!item || typeof item !== 'object')
79
78
  return item;
80
79
  if (ancestors.has(item))
81
- throw operationUnavailable();
80
+ throw operationBroken('Tool operation value has a cycle'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
82
81
  const prototype = Object.getPrototypeOf(item);
83
82
  if (!Array.isArray(item) && prototype !== Object.prototype && prototype !== null) {
84
- throw operationUnavailable();
83
+ throw operationBroken('Tool operation value holds an object that is not plain data'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
84
+ }
85
+ if (Object.hasOwn(item, '__proto__')) {
86
+ throw operationBroken('Tool operation value has an own __proto__ key'); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
85
87
  }
86
- if (Object.hasOwn(item, '__proto__'))
87
- throw operationUnavailable();
88
88
  ancestors.add(item);
89
89
  try {
90
90
  if (Array.isArray(item))
91
91
  return item.map(normalize);
92
92
  const entries = Object.entries(Object.getOwnPropertyDescriptors(item));
93
- if (entries.some(([, descriptor]) => descriptor.get || descriptor.set))
94
- throw operationUnavailable();
93
+ const accessor = entries.find(([, descriptor]) => descriptor.get || descriptor.set);
94
+ if (accessor) {
95
+ throw operationBroken(`Tool operation value has a getter or setter at '${accessor[0]}'`); // labels-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
96
+ }
95
97
  return Object.fromEntries(entries
96
98
  .filter(([, descriptor]) => descriptor.enumerable && descriptor.value !== undefined)
97
99
  .map(([key, descriptor]) => [key, normalize(descriptor.value)]));
@@ -382,13 +382,20 @@ async function* remoteAuthAndPreBody(args) {
382
382
  ...(authRes.audience ? { audience: authRes.audience } : {}),
383
383
  };
384
384
  }
385
+ /** A thrown request's code: a kind with a code of its own, else `request_failed`. */
386
+ const THROWN_CODES = {
387
+ network: 'network_error',
388
+ cancelled: 'cancelled',
389
+ timeout: 'timeout',
390
+ };
385
391
  function* thrownOutcome(err, checks) {
386
392
  if (errorKind(err) === 'blocked') {
387
393
  const blocked = networkBlockedEvent();
388
394
  checks.record(blocked);
389
395
  return failureOutcome(yield* networkBlocked(err, blocked), false);
390
396
  }
391
- return failureOutcome({ code: 'network_error', kind: 'network', message: messageOf(err) }, false);
397
+ const kind = errorKind(err);
398
+ return failureOutcome({ code: THROWN_CODES[kind] ?? 'request_failed', kind, message: messageOf(err) }, false);
392
399
  }
393
400
  const OMIT_CREDENTIAL = '[omitted - credential]';
394
401
  /** The most of an error or unparseable body quoted back in a failure message. */
@@ -646,6 +646,7 @@ const HITS = fields('error', 'Hits', 'Each rule that matched.', {
646
646
  open: true,
647
647
  },
648
648
  severity: attr('error', 'Severity', 'text', 'How serious the rule says a match is.', SEVERITY_OPTIONS),
649
+ signal: attr('error', 'Signal', 'text', "What made a `tool_instructions` match one, or how a host detector's `find` failed: `find_threw` or `find_result`."),
649
650
  start: attr('error', 'From', 'number', 'Where the match starts in the checked text.'),
650
651
  end: attr('error', 'To', 'number', 'Where the match ends in the checked text.'),
651
652
  match: attr('error', 'Match', 'text', 'The matched text, exact. Kept only when the profile records match previews.'),
@@ -680,6 +681,7 @@ const TRACE_EVENTS = {
680
681
  depth: attr('tool', 'Hops', 'number', "Tool calls between the user's turn and this text: 1 for a direct call, more through another agent."),
681
682
  }),
682
683
  error: attr('error', 'Detail', 'content', "The guardrail's internal reason."),
684
+ error_summary: attr('error', 'Failure', 'text', "Which host detector failed, how, and its error id, in the kernel's words only. The host stream's reason ends with the same id."),
683
685
  },
684
686
  },
685
687
  'theorem.attempt.retry': {
@@ -1,5 +1,4 @@
1
- import { TheoremError } from '../../guardrails/error.js';
2
- import { networkError } from './upstream-tap.js';
1
+ import { networkError, TheoremError } from '../../guardrails/error.js';
3
2
  const BACKOFF_MS = [1000, 2000, 4000, 8000];
4
3
  /** All the waiting one call may do between tries; a wait that would pass it sends the last answer back. */
5
4
  export const WAIT_BUDGET_MS = 5000;
@@ -3,6 +3,4 @@ export declare function tapeHeaderValue(key: string, value: string): string;
3
3
  export declare function tapeHeaders(headers?: HeadersInit): Record<string, string>;
4
4
  export declare function throwRow(err: unknown): Record<string, unknown>;
5
5
  export declare function tapFetch(tap: ModelRequest['tapUpstream'], send?: typeof fetch, keySlot?: KeySlot): typeof fetch;
6
- /** fetch rejects only when no response came back, so any non-abort, non-timeout rejection is `network`. */
7
- export declare function networkError(err: unknown): unknown;
8
6
  export declare function networkFetch(send?: typeof fetch): typeof fetch;
@@ -1,4 +1,4 @@
1
- import { describeError, isAbortError, isTimeoutError, TheoremError, } from '../../guardrails/error.js';
1
+ import { sendOrNetworkError } from '../../guardrails/error.js';
2
2
  const SECRET_HEADER = /key|auth|cookie|secret|token/i;
3
3
  export function tapeHeaderValue(key, value) {
4
4
  if (SECRET_HEADER.test(key)) {
@@ -66,20 +66,6 @@ export function tapFetch(tap, send = fetch, keySlot) {
66
66
  }
67
67
  };
68
68
  }
69
- /** fetch rejects only when no response came back, so any non-abort, non-timeout rejection is `network`. */
70
- export function networkError(err) {
71
- if (err instanceof TheoremError || isAbortError(err) || isTimeoutError(err)) {
72
- return err;
73
- }
74
- return new TheoremError('network', describeError(err), { cause: err });
75
- }
76
69
  export function networkFetch(send = fetch) {
77
- return async (url, init) => {
78
- try {
79
- return await send(url, init);
80
- }
81
- catch (err) {
82
- throw networkError(err);
83
- }
84
- };
70
+ return (url, init) => sendOrNetworkError(send, url, init);
85
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/agents",
3
- "version": "0.3.4-next.522.g9e684cd",
3
+ "version": "0.3.4-next.528.g44c76f9",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, registered tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",