@orkestrel/agent 0.0.6 → 0.0.8

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.
@@ -333,6 +333,24 @@ export declare interface AgentContextOptions {
333
333
  readonly conversations?: ConversationManagerInterface;
334
334
  }
335
335
 
336
+ /**
337
+ * Thrown synchronously by an {@link AgentInterface}'s `stream()` when a concurrent run would
338
+ * corrupt SHARED per-agent accounting — carries a machine-readable `code`.
339
+ *
340
+ * @remarks
341
+ * A run already in flight on the same agent, PLUS a construction-level `window` (a shared
342
+ * context budget) or a construction-level `budget` with no per-run override (a shared cost
343
+ * budget), means a second concurrent `stream()` would race its charges against the same
344
+ * shared instance — corrupting the accounting. `code` is `'CONCURRENCY'` (the only condition
345
+ * so far). Use separate agents, or per-run `budget` overrides with no `window`, for genuinely
346
+ * concurrent runs. Narrow a caught value with {@link isAgentError} and branch on `error.code`.
347
+ */
348
+ export declare class AgentError extends Error {
349
+ /** The machine-readable condition — `'CONCURRENCY'`: a concurrent run on a shared accounting agent. */
350
+ readonly code: 'CONCURRENCY';
351
+ constructor(code: 'CONCURRENCY', message: string);
352
+ }
353
+
336
354
  /**
337
355
  * The push observation surface of an {@link AgentInterface} (AGENTS §13) — the
338
356
  * lifecycle + usage/tool moments a fire-and-forget observer (logging, metrics,
@@ -407,16 +425,32 @@ export declare interface AgentInterface {
407
425
  * Run the turn to completion, discarding the live chunks — drains the shared
408
426
  * stream and resolves the settled outcome.
409
427
  *
428
+ * @remarks
429
+ * A concurrent run on a shared accounting agent throws an
430
+ * {@link import('./errors.js').AgentError} (`code: 'CONCURRENCY'`) — and it throws
431
+ * SYNCHRONOUSLY, before any `Promise` is returned. A fire-and-forget
432
+ * `agent.generate().catch(...)` therefore will NOT catch it (the throw happens on the call
433
+ * itself, ahead of the `.catch` ever attaching) — `await` the call (inside a `try`/`catch`)
434
+ * or wrap the call expression itself in `try`/`catch`.
435
+ *
410
436
  * @param options - Optional per-run {@link AgentRunOptions} (e.g. `think`); omitted ⇒ defaults
411
437
  * @returns The settled {@link AgentResult} (`partial: true` when cancelled)
438
+ * @throws {AgentError} Synchronously, with `code: 'CONCURRENCY'`, for a concurrent run
412
439
  */
413
440
  generate(options?: AgentRunOptions): Promise<AgentResult>;
414
441
  /**
415
442
  * Run the turn as a live stream — iterate `events` for {@link AgentChunk}s and
416
443
  * `await result` for the settled outcome.
417
444
  *
445
+ * @remarks
446
+ * Like `generate()`, a concurrent run on a shared accounting agent throws an
447
+ * {@link import('./errors.js').AgentError} (`code: 'CONCURRENCY'`) SYNCHRONOUSLY — before the
448
+ * {@link AgentStreamInterface} handle is even returned, so it cannot be caught by chaining
449
+ * off the (never-produced) handle; wrap the call itself in `try`/`catch`.
450
+ *
418
451
  * @param options - Optional per-run {@link AgentRunOptions} (e.g. `think`); omitted ⇒ defaults
419
452
  * @returns A live {@link AgentStreamInterface} handle (events + result + abort)
453
+ * @throws {AgentError} Synchronously, with `code: 'CONCURRENCY'`, for a concurrent run
420
454
  */
421
455
  stream(options?: AgentRunOptions): AgentStreamInterface;
422
456
  /**
@@ -525,6 +559,16 @@ export declare interface AgentJobInput {
525
559
  * reaches the window AND the active conversation is summarizable, COMPACTS the active
526
560
  * conversation + continues on the rebuilt smaller view — compact-and-continue, distinct from
527
561
  * `budget`'s hard abort. Omitted ⇒ no auto-compaction.
562
+ * - `strict` — when `true`, a summarizer failure during AUTOMATIC compaction ABORTS the run
563
+ * (rethrown after the `compactError` event, propagating through `#run` to a genuine `error`
564
+ * settle) instead of skipping compaction and continuing over-window. Defaults to `false`
565
+ * (lenient — the prior, byte-for-byte behavior).
566
+ * - `instructions` — an optional pre-built {@link InstructionManagerInterface} forwarded to the
567
+ * agent's context; an empty one is created when omitted (mirrors {@link AgentContextOptions.instructions}).
568
+ * - `workspaces` — an optional pre-built {@link WorkspaceManagerInterface} forwarded to the
569
+ * agent's context; a fresh empty one is created when omitted (mirrors {@link AgentContextOptions.workspaces}).
570
+ * - `scope` — an optional initial active {@link ScopeInterface} forwarded to the agent's context
571
+ * (the build-time filter); `undefined` ⇒ no filtering (mirrors {@link AgentContextOptions.scope}).
528
572
  * - `on` — the reserved {@link EmitterHooks} key (§8): initial listeners for the agent's
529
573
  * {@link AgentEventMap}, wired at construction (e.g. `{ finish: (r) => log(r) }`).
530
574
  */
@@ -535,6 +579,12 @@ export declare interface AgentOptions {
535
579
  readonly system?: string;
536
580
  /** A pre-built tool registry the loop dispatches calls through; an empty one is created when omitted. */
537
581
  readonly tools?: ToolManagerInterface;
582
+ /** A pre-built instruction registry forwarded to the agent's context; an empty one is created when omitted. */
583
+ readonly instructions?: InstructionManagerInterface;
584
+ /** A pre-built workspace registry forwarded to the agent's context; a fresh empty one is created when omitted. */
585
+ readonly workspaces?: WorkspaceManagerInterface;
586
+ /** The initial active scope forwarded to the agent's context (the build-time filter); `undefined` ⇒ no filtering. */
587
+ readonly scope?: ScopeInterface;
538
588
  /** Max tool-iteration turns before the loop stops; defaults to `DEFAULT_AGENT_LIMIT`. */
539
589
  readonly limit?: number;
540
590
  /** A wall-clock deadline (ms) for the whole turn; its abort commits a partial result. */
@@ -572,6 +622,13 @@ export declare interface AgentOptions {
572
622
  * as the cost `budget`, but compaction is the ceiling action instead of abort. Omit to disable.
573
623
  */
574
624
  readonly window?: BudgetInterface<readonly MessageInterface[]>;
625
+ /**
626
+ * When `true`, a summarizer failure during AUTOMATIC compaction ABORTS the run — the
627
+ * `compactError` event still fires, then the caught error is RETHROWN so the run settles
628
+ * `error` instead of continuing over-window. Defaults to `false` (lenient — the run
629
+ * continues over-window, byte-for-byte the prior behavior).
630
+ */
631
+ readonly strict?: boolean;
575
632
  }
576
633
 
577
634
  /**
@@ -1096,11 +1153,16 @@ export declare interface CompactionState {
1096
1153
  * `keep` overrides the conversation's configured retained-tail size for THIS compaction only
1097
1154
  * (the older `count - keep` live messages fold; when `count <= keep` nothing folds and
1098
1155
  * `compact()` is a no-op returning `undefined`). Omitted ⇒ the conversation's own `keep`
1099
- * (its option, or `DEFAULT_CONVERSATION_KEEP`) applies.
1156
+ * (its option, or `DEFAULT_CONVERSATION_KEEP`) applies. `sections` overrides the conversation's
1157
+ * configured `sections` cap for THIS compaction only — after the new section is pushed, an
1158
+ * overflow past `sections` folds the oldest sections into one merged section. Omitted ⇒ the
1159
+ * conversation's own `sections` cap (or unlimited) applies.
1100
1160
  */
1101
1161
  export declare interface CompactOptions {
1102
1162
  /** Override the retained-tail size for this compaction; omitted ⇒ the conversation's own `keep`. */
1103
1163
  readonly keep?: number;
1164
+ /** Override the `sections` cap for this compaction; omitted ⇒ the conversation's own cap (or unlimited). */
1165
+ readonly sections?: number;
1104
1166
  }
1105
1167
 
1106
1168
  /**
@@ -1274,20 +1336,23 @@ export declare const CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages]
1274
1336
 
1275
1337
  /**
1276
1338
  * Thrown by a {@link ConversationInterface}'s `compact()` when the conversation has no
1277
- * {@link ConversationSummarizer} to fold its messages with — carries a machine-readable
1278
- * `code`.
1339
+ * {@link ConversationSummarizer} to fold its messages with, or when its `sections` cap is
1340
+ * structurally invalid — carries a machine-readable `code`.
1279
1341
  *
1280
1342
  * @remarks
1281
1343
  * Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
1282
1344
  * regenerates the rollup); a conversation created without one can still store + `view()` its
1283
- * live tail, but a `compact()` is a programmer error (§12) and throws this. `code` is
1284
- * `'SUMMARIZER'` (the only condition so far). Narrow a caught value with
1345
+ * live tail, but a `compact()` is a programmer error (§12) and throws this with `'SUMMARIZER'`.
1346
+ * A `sections` cap (§F2, on {@link import('./types.js').ConversationOptions} /
1347
+ * {@link import('./types.js').ConversationManagerOptions} /
1348
+ * {@link import('./types.js').CompactOptions}) must be `>= 1` — a sub-1 cap is a programmer
1349
+ * error and throws this with `'SECTIONS'`. Narrow a caught value with
1285
1350
  * {@link isConversationError} and branch on `error.code`.
1286
1351
  */
1287
1352
  export declare class ConversationError extends Error {
1288
- /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer. */
1289
- readonly code: 'SUMMARIZER';
1290
- constructor(code: 'SUMMARIZER', message: string);
1353
+ /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap. */
1354
+ readonly code: 'SUMMARIZER' | 'SECTIONS';
1355
+ constructor(code: 'SUMMARIZER' | 'SECTIONS', message: string);
1291
1356
  }
1292
1357
 
1293
1358
  /**
@@ -1295,9 +1360,11 @@ export declare class ConversationError extends Error {
1295
1360
  * moments a fire-and-forget observer subscribes to via `conversation.emitter.on`.
1296
1361
  *
1297
1362
  * @remarks
1298
- * `compact` carries the newly-folded {@link SectionInterface}; `summary` carries the
1299
- * regenerated conversation rollup (refreshed on each compaction); `rehydrate` carries the
1300
- * `id` of a section whose originals were pulled back. Listener isolation is the emitter's
1363
+ * `compact` carries the newly-folded {@link SectionInterface}; `collapse` carries a section
1364
+ * created by folding multiple OLDER sections together (a bounded-`sections` cap enforcement,
1365
+ * distinct from `compact`'s fresh live-tail fold); `summary` carries the regenerated
1366
+ * conversation rollup (refreshed on each compaction); `rehydrate` carries the `id` of a
1367
+ * section whose originals were pulled back. Listener isolation is the emitter's
1301
1368
  * (§13): every event is emitted directly and a listener throw is routed to the emitter's
1302
1369
  * `error` handler (the `error` option), never onto this map, so a buggy observer can never
1303
1370
  * corrupt a compaction. A `type` alias (not `interface extends EventMap`, §4.5) so the
@@ -1310,6 +1377,11 @@ export declare type ConversationEventMap = {
1310
1377
  readonly summary: readonly [summary: string];
1311
1378
  /** A section's original messages were pulled back — the section's `id`. */
1312
1379
  readonly rehydrate: readonly [id: string];
1380
+ /**
1381
+ * The bounded-`sections` cap folded the oldest sections into ONE merged section — the
1382
+ * merged {@link SectionInterface} that replaced them.
1383
+ */
1384
+ readonly collapse: readonly [section: SectionInterface];
1313
1385
  };
1314
1386
 
1315
1387
  /**
@@ -1321,7 +1393,8 @@ export declare type ConversationEventMap = {
1321
1393
  * `id` is the conversation's identity (minted when omitted). `summarize` OVERRIDES the
1322
1394
  * manager's default {@link ConversationSummarizer} for this conversation (omitted ⇒ the
1323
1395
  * manager's default flows in). `keep` overrides the manager's default retained-tail size.
1324
- * `on` is the §8 reserved key (initial {@link ConversationEventMap} listeners). `snapshot` is
1396
+ * `sections` overrides the manager's default `sections` cap. `on` is the §8 reserved key
1397
+ * (initial {@link ConversationEventMap} listeners). `snapshot` is
1325
1398
  * the construction-time hydration seam — a {@link ConversationSnapshot} whose `id` / `summary` /
1326
1399
  * `sections` / live tail are RESTORED into the new conversation (the live `summarize` / `keep` /
1327
1400
  * `on` re-supplied alongside it), the conversation analogue of {@link WorkspaceInput.seed} that a
@@ -1335,6 +1408,8 @@ export declare interface ConversationInput {
1335
1408
  readonly summarize?: ConversationSummarizer;
1336
1409
  /** Overrides the manager's default retained-tail size for this conversation. */
1337
1410
  readonly keep?: number;
1411
+ /** Overrides the manager's default `sections` cap for this conversation. */
1412
+ readonly sections?: number;
1338
1413
  readonly on?: EmitterHooks<ConversationEventMap>;
1339
1414
  /** A {@link ConversationSnapshot} to hydrate FROM (its `id` / `summary` / `sections` / live tail restored); the analogue of `WorkspaceInput.seed`. */
1340
1415
  readonly snapshot?: ConversationSnapshot;
@@ -1459,6 +1534,13 @@ export declare interface ConversationInterface {
1459
1534
  * resolves the new section. Requires a {@link ConversationSummarizer} — THROWS a
1460
1535
  * {@link import('./errors.js').ConversationError} when none was supplied.
1461
1536
  *
1537
+ * @remarks
1538
+ * When a `sections` cap is set and the fold pushes the section count over it, an overflow
1539
+ * merge step folds the oldest sections into one — if THAT merge's `summarize` call throws,
1540
+ * the merge is skipped (sections transiently sit at `cap + 1`, no loss) but the rollup still
1541
+ * regenerates over the current unmerged sections (never left stale) before the error
1542
+ * propagates; the next successful `compact()` self-heals the section count back to `cap`.
1543
+ *
1462
1544
  * @param options - Optional {@link CompactOptions} (`keep` overrides the retained-tail size)
1463
1545
  * @returns The new {@link SectionInterface}, or `undefined` when nothing folded
1464
1546
  */
@@ -1651,13 +1733,17 @@ export declare interface ConversationManagerInterface {
1651
1733
  * (a per-`add` {@link ConversationInput.summarize} overrides it); a conversation created
1652
1734
  * with neither cannot `compact` (it throws a `ConversationError`). `keep` is the default
1653
1735
  * retained-tail size (a per-`add` {@link ConversationInput.keep} overrides it), defaulting
1654
- * to {@link import('./constants.js').DEFAULT_CONVERSATION_KEEP}.
1736
+ * to {@link import('./constants.js').DEFAULT_CONVERSATION_KEEP}. `sections` is the default cap
1737
+ * on a created conversation's compacted `sections` list (a per-`add` {@link ConversationInput.sections}
1738
+ * overrides it); omitted ⇒ unlimited.
1655
1739
  */
1656
1740
  export declare interface ConversationManagerOptions {
1657
1741
  /** The default summarizer for conversations this manager creates (a per-`add` override wins). */
1658
1742
  readonly summarize?: ConversationSummarizer;
1659
1743
  /** The default retained-tail size (a per-`add` override wins); defaults to `DEFAULT_CONVERSATION_KEEP`. */
1660
1744
  readonly keep?: number;
1745
+ /** The default `sections` cap for conversations this manager creates (a per-`add` override wins); omitted ⇒ unlimited. */
1746
+ readonly sections?: number;
1661
1747
  /**
1662
1748
  * The optional durable {@link ConversationStoreInterface} backing
1663
1749
  * {@link ConversationManagerInterface.open} / {@link ConversationManagerInterface.save} — a memory
@@ -1681,7 +1767,10 @@ export declare interface ConversationManagerOptions {
1681
1767
  * live tail, it just cannot fold). `keep` is how many recent live messages a `compact()`
1682
1768
  * retains VERBATIM (folding only the older ones); it defaults to
1683
1769
  * {@link import('./constants.js').DEFAULT_CONVERSATION_KEEP} (`0` — a manual `compact()`
1684
- * folds the WHOLE current live tail into one section).
1770
+ * folds the WHOLE current live tail into one section). `sections` is an optional cap on the
1771
+ * compacted `sections` list — when set (`>= 1`), a `compact()` that would leave more than
1772
+ * `sections` sections FOLDS the oldest overflow into ONE merged section so the list never
1773
+ * exceeds `sections`, emitting `collapse`; omitted ⇒ unlimited (the prior behavior).
1685
1774
  */
1686
1775
  export declare interface ConversationOptions {
1687
1776
  readonly id?: string;
@@ -1692,6 +1781,8 @@ export declare interface ConversationOptions {
1692
1781
  readonly summarize?: ConversationSummarizer;
1693
1782
  /** Recent live messages kept verbatim on `compact`; defaults to `DEFAULT_CONVERSATION_KEEP` (`0`). */
1694
1783
  readonly keep?: number;
1784
+ /** A cap (`>= 1`) on the compacted `sections` list; overflow folds into one merged section. Omitted ⇒ unlimited. */
1785
+ readonly sections?: number;
1695
1786
  }
1696
1787
 
1697
1788
  /**
@@ -2784,19 +2875,26 @@ export declare function escapeRegExp(value: string): string;
2784
2875
  * (the {@link import('./types.js').AgentOptions} `window`).
2785
2876
  *
2786
2877
  * @remarks
2787
- * Sums {@link estimateTokens} over each message's `content` (the `ceil(length / 4)` char
2788
- * heuristic), so it is deterministic and provider-free — the same messages always yield the
2789
- * same estimate, with an empty batch `0`. It is the fully-swappable default an agent's
2790
- * auto-compaction context budget charges each turn's new messages through; a caller wanting a
2791
- * sharper count supplies its own `consume` to `createBudget` instead. Total — never throws.
2878
+ * Sums, per message, {@link estimateTokens} over its `content` (the `ceil(length / 4)` char
2879
+ * heuristic) PLUS {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (a fixed per-message
2880
+ * role/framing overhead) PLUS, when present, {@link estimateTokens} over its JSON-stringified
2881
+ * `calls` PLUS `images.length * `{@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} (a coarse,
2882
+ * deliberately-approximate per-image cost — a base64 length is NOT a token proxy). Deterministic
2883
+ * and provider-free — the same messages always yield the same estimate, with an empty batch `0`.
2884
+ * It is the fully-swappable default an agent's auto-compaction context budget charges each
2885
+ * turn's new messages through; a caller wanting a sharper count supplies its own `consume` to
2886
+ * `createBudget` instead. Total — never throws: a `calls` `JSON.stringify` that throws (a
2887
+ * circular `ToolCall.arguments`) is caught and replaced with a conservative fixed contribution of
2888
+ * {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (the same per-message overhead scale)
2889
+ * instead of estimating the (unreachable) serialized length.
2792
2890
  *
2793
2891
  * @param messages - The messages to estimate (a turn's appended assistant + tool messages)
2794
- * @returns The summed estimated token count (`Σ estimateTokens(m.content)`; `0` when empty)
2892
+ * @returns The summed estimated token count (`0` when empty)
2795
2893
  *
2796
2894
  * @example
2797
2895
  * ```ts
2798
2896
  * estimateMessages([]) // 0
2799
- * estimateMessages([{ id: '1', role: 'user', content: 'hello' }]) // 2
2897
+ * estimateMessages([{ id: '1', role: 'user', content: 'hello' }]) // 6 (2 content + 4 overhead)
2800
2898
  * ```
2801
2899
  */
2802
2900
  export declare function estimateMessages(messages: readonly MessageInterface[]): number;
@@ -2958,6 +3056,18 @@ export declare type FileState = 'created' | 'modified' | 'loaded' | 'deleted';
2958
3056
  */
2959
3057
  export declare function filterAllowList<T>(allow: readonly string[] | undefined, items: readonly T[], key: (item: T) => string): readonly T[];
2960
3058
 
3059
+ /**
3060
+ * The coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
3061
+ * charges for each attached image.
3062
+ *
3063
+ * @remarks
3064
+ * A base64 image payload's LENGTH is NOT a reliable token proxy (a vision model's actual image
3065
+ * token cost depends on resolution / tiling, not byte size), so this is a fixed, coarse
3066
+ * per-image estimate rather than a derivation from `image.length` — a planning heuristic, not an
3067
+ * exact count.
3068
+ */
3069
+ export declare const IMAGE_TOKEN_ESTIMATE = 512;
3070
+
2961
3071
  /**
2962
3072
  * Infer a fenced-code language tag from a file path's extension — what a workspace text file
2963
3073
  * (or the {@link fencedFile} renderer) renders its content block as.
@@ -3190,6 +3300,23 @@ export declare interface InstructionManagerOptions {
3190
3300
  readonly format?: ContextSectionFormat<InstructionInterface>;
3191
3301
  }
3192
3302
 
3303
+ /**
3304
+ * Narrow an unknown caught value to an {@link AgentError}.
3305
+ *
3306
+ * @param value - The value to test (typically a `catch` binding)
3307
+ * @returns `true` when `value` is an {@link AgentError}
3308
+ *
3309
+ * @example
3310
+ * ```ts
3311
+ * try {
3312
+ * agent.stream()
3313
+ * } catch (error) {
3314
+ * if (isAgentError(error) && error.code === 'CONCURRENCY') useSeparateAgents()
3315
+ * }
3316
+ * ```
3317
+ */
3318
+ export declare function isAgentError(value: unknown): value is AgentError;
3319
+
3193
3320
  /**
3194
3321
  * Narrow an unknown caught value to an {@link AgentJobError}.
3195
3322
  *
@@ -3601,6 +3728,14 @@ export declare class MemoryWorkspaceStore implements WorkspaceStoreInterface {
3601
3728
  delete(id: string): Promise<void>;
3602
3729
  }
3603
3730
 
3731
+ /**
3732
+ * The estimated per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
3733
+ * adds on top of a message's content estimate — accounts for the fixed wire framing every
3734
+ * conversation turn carries (its role tag, delimiters) that {@link import('./helpers.js').estimateTokens}'s
3735
+ * content-only heuristic does not otherwise capture.
3736
+ */
3737
+ export declare const MESSAGE_TOKEN_OVERHEAD = 4;
3738
+
3604
3739
  /**
3605
3740
  * The minimal data needed to author a {@link MessageInterface} — the `id` is
3606
3741
  * assigned by the layer that stores it, so a caller supplies only role / content
@@ -3954,6 +4089,26 @@ export declare interface RunOutcome {
3954
4089
  exhausted: boolean;
3955
4090
  }
3956
4091
 
4092
+ /**
4093
+ * Sanitize a {@link TokenUsage} into safe, non-negative integers — the guard an agent's
4094
+ * abort-usage path applies to a provider's partial usage before it is charged against a
4095
+ * budget or folded into the run total.
4096
+ *
4097
+ * @remarks
4098
+ * Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
4099
+ * `-Infinity`) or a negative value floors to `0`; a fractional value floors to its
4100
+ * non-negative integer part. No upper cap is applied. Total — never throws.
4101
+ *
4102
+ * @param usage - The token usage to sanitize (e.g. a provider's abort-partial usage)
4103
+ * @returns A new {@link TokenUsage} with every field a safe non-negative integer
4104
+ *
4105
+ * @example
4106
+ * ```ts
4107
+ * sanitizeUsage({ prompt: -5, completion: NaN, total: 12.7 }) // { prompt: 0, completion: 0, total: 12 }
4108
+ * ```
4109
+ */
4110
+ export declare function sanitizeUsage(usage: TokenUsage): TokenUsage;
4111
+
3957
4112
  /**
3958
4113
  * A named, immutable filter over a richer context's items — three optional allow-lists,
3959
4114
  * one per category (`instructions` / `tools` / `files`), each keyed by that category's