theorum 0.1.6 → 0.1.7

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.
@@ -181,6 +181,7 @@ export interface Profile {
181
181
 
182
182
  ### Per-turn Interactions state
183
183
  - `TurnRequest.input`: Optional turn input object. If omitted, Theorum normalizes it to an empty input and still runs the profile/provider turn.
184
+ - `TurnRequest.signal`: Optional `AbortSignal`. When aborted, THEORUM stops the turn and cancels in-flight provider HTTP (Gemini fetch, OpenRouter `abortSignal`, speech fetch). Traces mark `cancelled: true`.
184
185
  - `TurnRequest.previousInteractionId`: Optional Google Interactions server-side conversation pointer. Theorum passes it through as `previous_interaction_id` for profiles using `geminiInteractions`.
185
186
  - `TurnRequest.store`: Optional Google Interactions storage override. If omitted, Theorum does not send `store`; provider/project policy remains the authority. If supplied, Theorum serializes the explicit boolean.
186
187
 
package/esm/mod.d.ts CHANGED
@@ -37,7 +37,7 @@
37
37
  * @module
38
38
  */
39
39
  import "./_dnt.polyfills.js";
40
- export { describeError, publicError, TheorumError, toErrorEvent } from './src/guardrails/error.js';
40
+ export { describeError, isAbortError, publicError, TheorumError, throwIfAborted, toErrorEvent, } from './src/guardrails/error.js';
41
41
  export type { QuotaSlotStatus } from './src/guardrails/quota.js';
42
42
  export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
43
43
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './src/guardrails/sanitize.js';
package/esm/mod.js CHANGED
@@ -37,7 +37,7 @@
37
37
  * @module
38
38
  */
39
39
  import "./_dnt.polyfills.js";
40
- export { describeError, publicError, TheorumError, toErrorEvent } from './src/guardrails/error.js';
40
+ export { describeError, isAbortError, publicError, TheorumError, throwIfAborted, toErrorEvent, } from './src/guardrails/error.js';
41
41
  export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './src/guardrails/quota.js';
42
42
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './src/guardrails/sanitize.js';
43
43
  export { runTurn } from './src/kernel/engine/runner.js';
@@ -28,6 +28,12 @@ declare const PUBLIC_FILE_SIZE = "That file is too large.";
28
28
  declare const PUBLIC_FILE_COUNT = "Too many files for one message.";
29
29
  /** Safe copy for unsupported generated image dimensions. */
30
30
  declare const PUBLIC_IMAGE_SIZE = "That image size isn't supported.";
31
+ /** Safe copy when the host aborts a turn. */
32
+ declare const PUBLIC_CANCELLED = "Cancelled.";
33
+ /** True when `err` is an abort (DOMException or Error named AbortError). */
34
+ declare function isAbortError(err: unknown): boolean;
35
+ /** Throw if `signal` is already aborted. */
36
+ declare function throwIfAborted(signal?: AbortSignal): void;
31
37
  /** Convert an unknown thrown value or internal message to user-safe text. */
32
38
  declare function publicError(err: unknown): string;
33
39
  /** Raw diagnostic text for hosts, traces, and logs (never shown to end users). */
@@ -42,4 +48,4 @@ declare function toErrorEvent(err: unknown): {
42
48
  error: string;
43
49
  errorInternal: string;
44
50
  };
45
- export { describeError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, };
51
+ export { describeError, isAbortError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_CANCELLED, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, throwIfAborted, toErrorEvent, UPSTREAM_FAILED, };
@@ -31,10 +31,33 @@ const PUBLIC_FILE_SIZE = 'That file is too large.';
31
31
  const PUBLIC_FILE_COUNT = 'Too many files for one message.';
32
32
  /** Safe copy for unsupported generated image dimensions. */
33
33
  const PUBLIC_IMAGE_SIZE = "That image size isn't supported.";
34
+ /** Safe copy when the host aborts a turn. */
35
+ const PUBLIC_CANCELLED = 'Cancelled.';
36
+ /** True when `err` is an abort (DOMException or Error named AbortError). */
37
+ function isAbortError(err) {
38
+ if (!err || typeof err !== 'object') {
39
+ return false;
40
+ }
41
+ const name = err.name;
42
+ return name === 'AbortError';
43
+ }
44
+ /** Throw if `signal` is already aborted. */
45
+ function throwIfAborted(signal) {
46
+ if (!signal?.aborted) {
47
+ return;
48
+ }
49
+ const { reason } = signal;
50
+ if (isAbortError(reason)) {
51
+ throw reason;
52
+ }
53
+ throw new DOMException('The operation was aborted.', 'AbortError');
54
+ }
34
55
  const EXACT = {
35
56
  [UPSTREAM_FAILED]: PUBLIC_UNAVAILABLE,
36
57
  'empty Gemini stream': PUBLIC_UNAVAILABLE,
37
58
  'canary leaked': PUBLIC_CANARY,
59
+ 'The operation was aborted.': PUBLIC_CANCELLED,
60
+ 'This operation was aborted': PUBLIC_CANCELLED,
38
61
  'Turn withheld: egress disclosure violation': PUBLIC_CANARY,
39
62
  'expected JSON object': 'Something was wrong with that request.',
40
63
  'user input cannot be placed in the system block': PUBLIC_GENERIC,
@@ -98,8 +121,12 @@ const ALREADY_PUBLIC = new Set([
98
121
  PUBLIC_FILE_SIZE,
99
122
  PUBLIC_FILE_COUNT,
100
123
  PUBLIC_IMAGE_SIZE,
124
+ PUBLIC_CANCELLED,
101
125
  ]);
102
126
  function publicText(text) {
127
+ if (/aborted/i.test(text)) {
128
+ return PUBLIC_CANCELLED;
129
+ }
103
130
  if (ALREADY_PUBLIC.has(text)) {
104
131
  return text;
105
132
  }
@@ -116,6 +143,9 @@ function publicText(text) {
116
143
  }
117
144
  /** Convert an unknown thrown value or internal message to user-safe text. */
118
145
  function publicError(err) {
146
+ if (isAbortError(err)) {
147
+ return PUBLIC_CANCELLED;
148
+ }
119
149
  if (typeof err === 'string') {
120
150
  return publicText(err);
121
151
  }
@@ -146,4 +176,4 @@ function toErrorEvent(err) {
146
176
  errorInternal: describeError(err),
147
177
  };
148
178
  }
149
- export { describeError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, };
179
+ export { describeError, isAbortError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_CANCELLED, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, throwIfAborted, toErrorEvent, UPSTREAM_FAILED, };
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { describeError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
11
+ export { describeError, isAbortError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_CANCELLED, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, throwIfAborted, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
13
  export type { QuotaSlotStatus } from './quota.js';
14
14
  export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import "../../_dnt.polyfills.js";
11
- export { describeError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
11
+ export { describeError, isAbortError, PUBLIC_ACTION, PUBLIC_CANARY, PUBLIC_CANCELLED, PUBLIC_FILE_COUNT, PUBLIC_FILE_SIZE, PUBLIC_FILE_TYPE, PUBLIC_GENERIC, PUBLIC_IMAGE_SIZE, PUBLIC_UNAVAILABLE, publicError, TheorumError, throwIfAborted, toErrorEvent, UPSTREAM_FAILED, } from './error.js';
12
12
  export { injectionSpans } from './injection.js';
13
13
  export { clientIp, quotaMessage, releaseSlot, resetSlots, skipQuota, takeSlot, } from './quota.js';
14
14
  export { PROJECT_ID_MAX, sanitizeProjectId, sanitizeText, sanitizeTurnRequest, } from './sanitize.js';
@@ -1,4 +1,4 @@
1
- import { TheorumError, toErrorEvent } from '../../../guardrails/error.js';
1
+ import { TheorumError, throwIfAborted, toErrorEvent } from '../../../guardrails/error.js';
2
2
  import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
3
3
  import { resolveTurn } from '../../registry/resolve.js';
4
4
  import { getStructured } from '../../registry/schemas.js';
@@ -191,6 +191,7 @@ async function* runAttemptsWithValidation(safe, profile, generation, system, pro
191
191
  currentReq: safe,
192
192
  };
193
193
  while (flow.currentAttempt <= maxRetries) {
194
+ throwIfAborted(safe.signal);
194
195
  const step = yield* executeSingleAttemptCycle({
195
196
  flow,
196
197
  state,
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
+ import { throwIfAborted } from '../../../guardrails/error.js';
10
11
  import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
11
12
  import { noopSink, writeTrace } from '../../../observability/trace.js';
12
13
  import { buildRecord } from '../../../observability/trace-record.js';
@@ -47,6 +48,7 @@ async function* runTurn(req, provider, sink = noopSink()) {
47
48
  let generation;
48
49
  try {
49
50
  const safe = sanitizeTurnRequest(req);
51
+ throwIfAborted(safe.signal);
50
52
  const { profile, generation: gen } = resolveTurn(safe);
51
53
  generation = gen;
52
54
  const { model: resolvedModel, geminiBucket, canary: turnCanary } = gen;
@@ -1,3 +1,4 @@
1
+ import { throwIfAborted } from '../../../guardrails/error.js';
1
2
  import { recordStepEvent } from './state.js';
2
3
  import { yieldProviderEvents } from './stream.js';
3
4
  import { executeDynamicDeclaration, findDynamicDeclaration, formatToolFinding, isActionableDynamicDeclaration, } from './tools.js';
@@ -11,7 +12,7 @@ async function* executeAutonomousStep(args, state, buffer = {
11
12
  holdLate: false,
12
13
  holdUserVisible: false,
13
14
  }) {
14
- const { profile, generation, system, provider, gemini } = args;
15
+ const { profile, generation, system, provider, gemini, signal } = args;
15
16
  const genForStep = { ...generation, history: state.currentHistory };
16
17
  const pendingTools = [];
17
18
  let latestStructured;
@@ -21,6 +22,7 @@ async function* executeAutonomousStep(args, state, buffer = {
21
22
  system,
22
23
  provider,
23
24
  gemini,
25
+ signal,
24
26
  })) {
25
27
  if (event.type === 'structured') {
26
28
  latestStructured = event.structured;
@@ -107,9 +109,10 @@ async function* executeAttempt(args) {
107
109
  const holdUserVisible = Boolean(profile.guardrails.egress?.enforce);
108
110
  const holdLate = Boolean(profile.outputs.validation) || holdUserVisible;
109
111
  while (!isStepLimitReached(stepInAttempt, generation.maxSteps)) {
112
+ throwIfAborted(args.safe.signal);
110
113
  stepInAttempt++;
111
114
  state.stepCount++;
112
- const stepResult = yield* executeAutonomousStep({ profile, generation, system, provider, gemini }, state, { holdLate, holdUserVisible });
115
+ const stepResult = yield* executeAutonomousStep({ profile, generation, system, provider, gemini, signal: args.safe.signal }, state, { holdLate, holdUserVisible });
113
116
  if (stepResult.latestStructured !== undefined) {
114
117
  latestStructured = stepResult.latestStructured;
115
118
  }
@@ -7,5 +7,6 @@ declare function yieldProviderEvents(args: {
7
7
  system: string;
8
8
  provider: ModelProvider;
9
9
  gemini: Record<string, unknown>[];
10
+ signal?: AbortSignal;
10
11
  }): AsyncGenerator<TurnEvent>;
11
12
  export { shouldSkipStreamEvent, systemFromProfile, yieldProviderEvents };
@@ -1,4 +1,4 @@
1
- import { publicError, toErrorEvent } from '../../../guardrails/error.js';
1
+ import { publicError, throwIfAborted, toErrorEvent } from '../../../guardrails/error.js';
2
2
  import { providerCompleteRequest } from '../../registry/provider-request.js';
3
3
  import { eventHasCanary, redactCanary } from '../boundary.js';
4
4
  import { dispatchModelTool } from './tools.js';
@@ -51,14 +51,17 @@ function* processNormalEvent(event, profile, generation) {
51
51
  }
52
52
  }
53
53
  async function* yieldProviderEvents(args) {
54
- const { profile, generation, system, provider, gemini } = args;
54
+ const { profile, generation, system, provider, gemini, signal } = args;
55
55
  const { canary } = generation;
56
+ throwIfAborted(signal);
56
57
  for await (const event of provider.complete({
57
58
  ...providerCompleteRequest(generation, system),
59
+ signal,
58
60
  tapGemini: (row) => {
59
61
  gemini.push(row);
60
62
  },
61
63
  })) {
64
+ throwIfAborted(signal);
62
65
  if (canary && eventHasCanary(event, canary)) {
63
66
  yield redactCanary(event, canary);
64
67
  yield toErrorEvent('canary leaked');
@@ -387,6 +387,11 @@ export interface TurnRequest {
387
387
  dynamicToolLoader?: DynamicToolLoader;
388
388
  /** Host-owned metadata preserved for traces; the kernel does not interpret it. */
389
389
  metadata?: Record<string, unknown>;
390
+ /**
391
+ * Optional abort signal. When aborted, THEORUM stops the turn and cancels
392
+ * in-flight provider HTTP where the adapter supports it.
393
+ */
394
+ signal?: AbortSignal;
390
395
  input?: TurnInput;
391
396
  toolInvoke?: {
392
397
  name: CustomToolId;
@@ -521,6 +526,8 @@ export interface ProviderCompleteRequest extends ProviderGenerationConfig {
521
526
  geminiBucket?: GeminiBucket;
522
527
  /** Scrubbed SSE / HTTP rows for traces. */
523
528
  tapGemini?: (row: Record<string, unknown>) => void;
529
+ /** Host abort signal — adapters should pass this into fetch / SDK calls. */
530
+ signal?: AbortSignal;
524
531
  }
525
532
  /** Minimal adapter contract every model provider must implement. */
526
533
  export interface ModelProvider {
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { publicError } from '../guardrails/error.js';
9
+ import { isAbortError, publicError } from '../guardrails/error.js';
10
10
  import { sanitizeText, sanitizeTurnRequest } from '../guardrails/sanitize.js';
11
11
  import { OMIT_CANARY } from '../kernel/engine/boundary.js';
12
12
  import { sha256 } from '../kernel/engine/hash.js';
@@ -109,14 +109,15 @@ async function buildRecord(args) {
109
109
  const lastErr = [...snapped].reverse().find((row) => row.type === 'error');
110
110
  const done = completedInteraction(gemini);
111
111
  const status = done?.status;
112
- const ok = !(thrown || lastErr) && status !== 'cancelled';
112
+ const aborted = isAbortError(thrown);
113
+ const ok = !(thrown || lastErr) && status !== 'cancelled' && !aborted;
113
114
  const record = {
114
115
  v: TRACE_VERSION,
115
116
  id: crypto.randomUUID(),
116
117
  ts: started,
117
118
  ms: Date.now() - started,
118
119
  streamed: true,
119
- cancelled: status === 'cancelled',
120
+ cancelled: status === 'cancelled' || aborted,
120
121
  previousInteractionId: safe.previousInteractionId ?? null,
121
122
  store: safe.store ?? null,
122
123
  profile: safe.profile,
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { TheorumError, UPSTREAM_FAILED } from '../guardrails/error.js';
9
+ import { isAbortError, TheorumError, UPSTREAM_FAILED } from '../guardrails/error.js';
10
10
  const ATTEMPTS = 3;
11
11
  const LAST_ATTEMPT = ATTEMPTS - 1;
12
12
  const BACKOFF_FIRST_MS = 1000;
@@ -39,6 +39,9 @@ function isTransientHttp(status) {
39
39
  status === HTTP_GATEWAY_TIMEOUT);
40
40
  }
41
41
  function isTransientThrown(err) {
42
+ if (isAbortError(err)) {
43
+ return false;
44
+ }
42
45
  return TRANSIENT_THROWN_RE.test(String(err));
43
46
  }
44
47
  function requireKey(vault, bucket) {
@@ -56,7 +59,9 @@ async function runWithBackoff(apiKey, run, wait, attempt) {
56
59
  return await run(apiKey);
57
60
  }
58
61
  catch (err) {
59
- if (!(isQuota(err) || isTransientThrown(err)) || attempt === LAST_ATTEMPT) {
62
+ if (isAbortError(err) ||
63
+ !(isQuota(err) || isTransientThrown(err)) ||
64
+ attempt === LAST_ATTEMPT) {
60
65
  throw err;
61
66
  }
62
67
  await wait(backoffMs(attempt));
@@ -103,11 +108,16 @@ async function fetchWithBackoff(args) {
103
108
  if (!isTransientHttp(last.status) || args.attempt === LAST_ATTEMPT) {
104
109
  return last;
105
110
  }
111
+ if (args.init.signal?.aborted) {
112
+ throw args.init.signal.reason instanceof Error
113
+ ? args.init.signal.reason
114
+ : new DOMException('The operation was aborted.', 'AbortError');
115
+ }
106
116
  await wait(backoffMs(args.attempt));
107
117
  return fetchWithBackoff({ ...args, attempt: args.attempt + 1 });
108
118
  }
109
119
  catch (err) {
110
- if (!isTransientThrown(err) || args.attempt === LAST_ATTEMPT) {
120
+ if (isAbortError(err) || !isTransientThrown(err) || args.attempt === LAST_ATTEMPT) {
111
121
  throw err;
112
122
  }
113
123
  await wait(backoffMs(args.attempt));
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
11
  import { jsonSchema, streamText, tool, } from 'ai';
12
- import { toErrorEvent } from '../guardrails/error.js';
12
+ import { isAbortError, toErrorEvent } from '../guardrails/error.js';
13
13
  import { tryStructured } from '../kernel/engine/delta.js';
14
14
  import { getTool } from '../kernel/registry/catalog.js';
15
15
  import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
@@ -499,6 +499,7 @@ function streamTextOptions(req, context) {
499
499
  tools: buildTools(req.dynamicTools),
500
500
  providerOptions: providerOptionsFor(req),
501
501
  includeRawChunks: true,
502
+ abortSignal: req.signal,
502
503
  onError: () => undefined,
503
504
  };
504
505
  }
@@ -544,6 +545,9 @@ async function* streamOpenRouter(req, config) {
544
545
  yield* finalEvents(req, acc);
545
546
  }
546
547
  catch (err) {
548
+ if (isAbortError(err)) {
549
+ throw err;
550
+ }
547
551
  yield* yieldCapturedRawEventsUnchecked(context);
548
552
  yield toErrorEvent(err);
549
553
  }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- import { TheorumError, toErrorEvent } from '../guardrails/error.js';
11
+ import { isAbortError, TheorumError, toErrorEvent } from '../guardrails/error.js';
12
12
  import { eventsFromComplete, eventsFromDelta, extractTokenEvent, groundingFromEvent, tryStructured, } from '../kernel/engine/delta.js';
13
13
  import { tapFetch } from './google-tap.js';
14
14
  import { toInteractionsBody } from './interactions.js';
@@ -121,7 +121,7 @@ async function* streamComplete(req, transport) {
121
121
  yield toErrorEvent('missing Gemini vault bucket for Interactions');
122
122
  return;
123
123
  }
124
- const res = await fetchGemini(INTERACTIONS_URL, { method: 'POST', body: JSON.stringify(toInteractionsBody(req)) }, req.geminiBucket, withTap(req, transport));
124
+ const res = await fetchGemini(INTERACTIONS_URL, { method: 'POST', body: JSON.stringify(toInteractionsBody(req)), signal: req.signal }, req.geminiBucket, withTap(req, transport));
125
125
  if (res.status !== HTTP_OK) {
126
126
  const errorBody = await res.text().catch(() => '');
127
127
  yield toErrorEvent(`Gemini HTTP ${String(res.status)}: ${errorBody}`);
@@ -148,6 +148,9 @@ async function* streamGuarded(req, transport) {
148
148
  yield* streamComplete(req, transport);
149
149
  }
150
150
  catch (err) {
151
+ if (isAbortError(err)) {
152
+ throw err;
153
+ }
151
154
  yield toErrorEvent(err);
152
155
  }
153
156
  }
@@ -68,6 +68,7 @@ async function requestSpeech(apiKey, text, req, config) {
68
68
  method: 'POST',
69
69
  headers: buildHeaders(apiKey, config),
70
70
  body: JSON.stringify(buildPayload(req, text, req.speech, config.voice)),
71
+ signal: req.signal,
71
72
  });
72
73
  }
73
74
  function* yieldSpeechSuccess(rawBytes, text, format) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theorum",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, dynamic tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",