@theokit/agents 8.5.2 → 8.7.0

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.
@@ -0,0 +1,534 @@
1
+ import { TheokitAgentError } from '@theokit/sdk/errors';
2
+ import { z } from 'zod';
3
+ import { TrustPosture } from '@theokit/sdk';
4
+ export { resolveEffectiveContextWindow as effectiveContextWindow } from '@theokit/sdk/compaction';
5
+
6
+ /**
7
+ * M73 — layered configuration, as a parameterised machine.
8
+ *
9
+ * ## The gap this closes
10
+ *
11
+ * The config module answered "load my framework's config file". It published no layering engine, did
12
+ * not let the SDK's through, and said nothing about directory trust. The evidence that this was a
13
+ * real gap and not a scope decision: a repository whose README forbids importing `@theokit/sdk`
14
+ * directly broke its own rule **six times**, and all six reach for config/trust/wiring primitives. A
15
+ * team that breaks its own rule rather than reimplement is the strongest signal that the door, not
16
+ * the willingness, was what was missing.
17
+ *
18
+ * ## What is policy and what is machine
19
+ *
20
+ * The VOCABULARY — which keys exist, which capabilities they grant, TOML or TS — is legitimately
21
+ * policy and stays with the product. The chain machine, the profile merge, the precedence report and
22
+ * the floor are identical in every agent product.
23
+ *
24
+ * The milestone's named risk was generalising too early and freezing another product's vocabulary.
25
+ * The mitigation is structural: **the layer chain is a parameter, never a constant.** No layer name
26
+ * from any consumer appears in this file, and a test asserts it by resolving a chain of names this
27
+ * repository has never heard of.
28
+ *
29
+ * ## Why it composes rather than implements
30
+ *
31
+ * `foldLayers` and `verifyLayerOrdering` are the SDK's, crossed by M67. Re-deriving the fold here
32
+ * would give two answers to "which layer wins", and the two would diverge silently — the exact
33
+ * defect this whole initiative exists to remove. What this adds is what the SDK deliberately does
34
+ * not: provenance per key, and the measured-vs-declared precedence report.
35
+ */
36
+ /** One layer of configuration, with the values it contributes. */
37
+ interface ConfigLayer {
38
+ readonly layer: string;
39
+ /** Higher wins. Omit across the whole chain to mean "this array is already the order". */
40
+ readonly precedence?: number;
41
+ readonly values: Readonly<Record<string, unknown>>;
42
+ }
43
+ /**
44
+ * Where each resolved key came from.
45
+ *
46
+ * For an overwritten key this is the winning layer's name. For an ACCUMULATED key it is every
47
+ * contributing layer, comma-separated — a union has no single winner, and naming only the last one
48
+ * would be a lie about where the other members came from.
49
+ */
50
+ type ProvenancePerKey = Readonly<Record<string, string>>;
51
+ /**
52
+ * Declared order versus the order the values actually proved.
53
+ *
54
+ * The consumer wrote this check by hand (`measuredPrecedenceChain`) because the engine did not offer
55
+ * it. A layer that declares high precedence and never wins a key is a config nobody is reading —
56
+ * usually a path that does not exist. Silence there is how a broken override survives for months.
57
+ */
58
+ interface PrecedenceReport {
59
+ /** The chain as the caller wrote it. */
60
+ readonly declared: readonly string[];
61
+ /**
62
+ * The layers that actually contributed at least one key, in chain order.
63
+ *
64
+ * PARTICIPATION, not victory. The first draft measured which layers WON a key, and a base
65
+ * `defaults` layer whose every value is later overridden then reported as absent from the measured
66
+ * chain — flagging the most normal arrangement in layered config as a divergence. Being
67
+ * overridden is what a defaults layer is for.
68
+ */
69
+ readonly measured: readonly string[];
70
+ /**
71
+ * Layers declared in the chain that contributed NO key at all.
72
+ *
73
+ * Not the same as "was overridden": being superseded is the normal fate of a defaults layer and
74
+ * says nothing is wrong. Contributing nothing usually means a path that does not exist — a config
75
+ * file nobody is reading — and that is the silence this report exists to break.
76
+ */
77
+ readonly declaredButSilent: readonly string[];
78
+ readonly diverges: boolean;
79
+ }
80
+ interface LayeredConfigInput<TSchema extends z.ZodType> {
81
+ readonly layers: readonly ConfigLayer[];
82
+ /** Validated AFTER folding — see {@link LayeredConfig.resolve}. */
83
+ readonly schema: TSchema;
84
+ /**
85
+ * Keys whose values UNION across layers instead of being overwritten.
86
+ *
87
+ * Opt-in per key, deliberately: some keys are lists a layer adds to (tools, plugins, allowlists)
88
+ * and overwriting them discards what a lower layer contributed — but defaulting to union would
89
+ * silently merge something the caller meant to replace.
90
+ */
91
+ readonly accumulatingKeys?: readonly string[];
92
+ }
93
+ interface LayeredConfigResult<TValue> {
94
+ readonly value: TValue;
95
+ readonly provenancePerKey: ProvenancePerKey;
96
+ readonly precedenceReport: PrecedenceReport;
97
+ }
98
+ /**
99
+ * Raised when a chain is built out of order.
100
+ *
101
+ * Refusing beats sorting silently: a caller who wrote the chain in the wrong order holds a belief
102
+ * about precedence that is wrong, and sorting it for them leaves the belief intact until it produces
103
+ * a surprise somewhere else.
104
+ */
105
+ declare class LayerOutOfOrderError extends TheokitAgentError {
106
+ readonly name = "LayerOutOfOrderError";
107
+ constructor(message: string);
108
+ }
109
+ /**
110
+ * The layering engine.
111
+ *
112
+ * A class with one static rather than a free function: it is the namespace the config surface hangs
113
+ * from (`LayeredConfig.resolve`), matching the `X.create()` shape the rest of this codebase uses, and
114
+ * it leaves room for the trust-store companion to join the same namespace without a second import.
115
+ */
116
+ declare const LayeredConfig: {
117
+ /**
118
+ * Fold the chain, validate the result, and report where everything came from.
119
+ *
120
+ * The schema is applied AFTER folding, never per layer. Validating each layer separately would
121
+ * force every file to be complete, which defeats layering: a project override that sets one key
122
+ * would have to restate the whole config.
123
+ *
124
+ * @throws {LayerOutOfOrderError} when a layer does not outrank the one before it.
125
+ */
126
+ resolve<TSchema extends z.ZodType>(input: LayeredConfigInput<TSchema>): LayeredConfigResult<z.infer<TSchema>>;
127
+ };
128
+
129
+ /**
130
+ * M73 — the per-directory trust store: a trust decision that survives the process.
131
+ *
132
+ * ## Why persisting it matters
133
+ *
134
+ * M68 made `settingSources`' `project` root require a `TrustPosture` — evidence, not a claim. But a
135
+ * posture computed fresh on every run is a question asked over and over, and a question asked every
136
+ * run is a question users learn to answer without reading.
137
+ *
138
+ * Persisting turns the stamp into a DECISION: recorded once, auditable afterwards, and answerable by
139
+ * "who trusted this directory, when, and on what basis" rather than by re-deriving it.
140
+ *
141
+ * ## Why the file permission is checked on READ
142
+ *
143
+ * This file decides whether a directory may run shell hooks. A store any other user can write is a
144
+ * store any other user can use to grant themselves that. Checking at write time only would leave a
145
+ * file whose mode was loosened afterwards looking fine — so the check is where the value is
146
+ * consumed, and a loose mode is REFUSED rather than repaired: silently tightening it hides that
147
+ * something changed the mode, which is the fact worth knowing.
148
+ */
149
+ /** What was decided about one directory. */
150
+ interface TrustRecord {
151
+ /** Absolute path of the trusted directory. */
152
+ readonly path: string;
153
+ /** ISO-8601 stamp of the decision. Injected by the caller — see {@link TrustStore.trust}. */
154
+ readonly decidedAt: string;
155
+ /** Free-form provenance: who or what decided (a username, a CI job, `--trust` on the CLI). */
156
+ readonly decidedBy: string;
157
+ readonly trusted: boolean;
158
+ }
159
+ /** Raised when the store's file mode would let another user grant themselves trust. */
160
+ declare class TrustStorePermissionsError extends TheokitAgentError {
161
+ readonly file: string;
162
+ readonly mode: number;
163
+ readonly name = "TrustStorePermissionsError";
164
+ constructor(file: string, mode: number);
165
+ }
166
+ declare class TrustStore {
167
+ private readonly file;
168
+ constructor(file: string);
169
+ /**
170
+ * Whether `path` carries a recorded decision to trust it.
171
+ *
172
+ * Denies on anything else — never recorded, recorded as refused, or unresolvable. A refusal on
173
+ * record (`trusted: false`) is a different fact from "never asked", and neither is trust.
174
+ */
175
+ isTrusted(path: string): boolean;
176
+ /**
177
+ * Read the store, refusing a file other users can write.
178
+ *
179
+ * A missing store is not an error — it is a machine that has trusted nothing yet, which is the
180
+ * correct starting state and the safe one.
181
+ */
182
+ read(): readonly TrustRecord[];
183
+ /**
184
+ * Record a decision about `path`, replacing any previous one for it.
185
+ *
186
+ * `decidedAt` and `decidedBy` are ARGUMENTS, not derived here (DIP): the clock and the identity
187
+ * belong to the caller, and baking `new Date()` in would make every assertion about the record
188
+ * depend on when the test ran.
189
+ *
190
+ * ASYNC because both `withFileLock` and `atomicWriteJson` are. Measured, not assumed: the first
191
+ * draft called them synchronously and `trust()` returned before the bytes landed, so an immediate
192
+ * `read()` saw an empty store. Same shape as the M71 pointer bug, and same cause — the SDK's
193
+ * `.d.ts` does not declare these, so nothing at compile time says they return a Promise
194
+ * (usetheodev/theokit-sdk#280).
195
+ */
196
+ trust(record: TrustRecord): Promise<void>;
197
+ /**
198
+ * The recorded posture for `path`, or an UNTRUSTED posture when nothing was recorded.
199
+ *
200
+ * Absence resolves to untrusted, never to "unknown, proceed". A store that answered "I do not
201
+ * know" would push the decision back to the caller, and the caller asking is what the store
202
+ * exists to answer.
203
+ */
204
+ postureFor<K extends string>(path: string, capabilities: readonly K[]): TrustPosture<K>;
205
+ private assertSafePermissions;
206
+ }
207
+
208
+ interface ExpandImportsInput {
209
+ readonly text: string;
210
+ /** Absolute path of the file `text` came from — imports resolve relative to its directory. */
211
+ readonly filePath: string;
212
+ /** Containment boundary. An import resolving outside it is kept literal. */
213
+ readonly rootDir: string;
214
+ readonly onWarn: (message: string) => void;
215
+ /**
216
+ * Frame imported content — e.g. `--- import: x ---` markers around it.
217
+ *
218
+ * Presentation belongs to the caller: these markers end up in the model's prompt, and a loader
219
+ * that dictated them would silently change what a product sends. Absent means the content is
220
+ * inlined bare, which is what every caller saw before this seam existed.
221
+ *
222
+ * @param name the reference as written, without the `@`
223
+ * @param content the imported file's content, already expanded
224
+ */
225
+ readonly wrap?: (name: string, content: string) => string;
226
+ /**
227
+ * Files the CALLER already read, which must not be inlined again.
228
+ *
229
+ * A walk that collects its files first and expands second — the ancestor-chain convention, where a
230
+ * product climbs from the working directory to the repository root — has already loaded some of
231
+ * the files an import may name. Without this, such a file lands in the prompt twice.
232
+ */
233
+ readonly alreadyLoaded?: readonly string[];
234
+ }
235
+ /**
236
+ * Replace every `@file.md` reference with that file's content, recursively.
237
+ *
238
+ * A reference that cannot be expanded — missing, outside the root, too deep, already visited — is
239
+ * left exactly as written. Keeping it literal rather than dropping it is what lets a user SEE that
240
+ * something did not expand; a silently deleted line reads as content nobody wrote.
241
+ */
242
+ declare function expandInstructionImports(input: ExpandImportsInput): string;
243
+
244
+ /**
245
+ * M74 — load a tree of project instruction files, with explicit ceilings.
246
+ *
247
+ * ## Why `compileProjectContext` does not cover this
248
+ *
249
+ * It reads ONE fixed file through the SDK: no depth or file budget, no frontmatter, no cycle guard,
250
+ * no truncation policy, no warning channel. A product that wants project-scoped instructions writes
251
+ * roughly 720 lines of mechanism — all of it identical between products, none of it about their
252
+ * domain.
253
+ *
254
+ * ## The containment check is a security control
255
+ *
256
+ * A symlink inside a project directory can point anywhere. Following one lets a repository the user
257
+ * just cloned inject the contents of `~/.ssh/config`, or any other readable file, straight into the
258
+ * model's system prompt. That is prompt injection with the filesystem as the vector, and every
259
+ * consumer that writes this loader by hand reintroduces it.
260
+ *
261
+ * `assertNoSymlinkEscape` is the SDK's, crossed in M67. Composing it is what makes the check the
262
+ * same one everywhere instead of four subtly different `realpath` comparisons.
263
+ *
264
+ * ## Failure is per FILE, not per tree
265
+ *
266
+ * A malformed frontmatter skips that file and warns. Failing the whole load would let one bad file
267
+ * in a deep tree silently disable every instruction the user wrote — the loudest possible failure
268
+ * producing the quietest possible outcome.
269
+ */
270
+ /** One loaded instruction file. */
271
+ interface InstructionBlock {
272
+ /** Path relative to `cwd`, for messages a human can act on. */
273
+ readonly path: string;
274
+ /** File body with the frontmatter removed. */
275
+ readonly content: string;
276
+ /** `paths:` from the frontmatter — the scopes this block applies to. Empty means unscoped. */
277
+ readonly scopes: readonly string[];
278
+ }
279
+ interface InstructionTreeBudget {
280
+ /** How deep below each root to descend. */
281
+ readonly maxDepth: number;
282
+ /** How many files to load in total. */
283
+ readonly maxFiles: number;
284
+ /** Total characters across all blocks. */
285
+ readonly maxChars: number;
286
+ }
287
+ interface LoadInstructionTreeInput {
288
+ readonly cwd: string;
289
+ /** Directories to walk, relative to `cwd` or absolute. Order is the caller's. */
290
+ readonly roots: readonly string[];
291
+ readonly budget: InstructionTreeBudget;
292
+ /**
293
+ * Where a skipped file, a refused symlink or an exhausted budget is reported.
294
+ *
295
+ * A channel rather than a throw: none of these should stop a load, and none of them should be
296
+ * silent either. Silence here is how a user's instruction file stops being read without anybody
297
+ * noticing.
298
+ */
299
+ readonly onWarn?: (message: string) => void;
300
+ /** File names to load. Defaults to the conventional two. */
301
+ readonly fileNames?: readonly string[];
302
+ }
303
+ interface InstructionTree {
304
+ readonly blocks: readonly InstructionBlock[];
305
+ /** True when a ceiling stopped the walk — the caller is seeing a partial tree. */
306
+ readonly truncated: boolean;
307
+ readonly count: number;
308
+ }
309
+ /**
310
+ * Walk `roots` and load the instruction files found, stopping at the declared ceilings.
311
+ *
312
+ * Cycles are broken by INODE, not by path: a symlink loop produces infinitely many distinct paths
313
+ * for the same file, so a path-keyed `seen` set never terminates. The inode is what identifies the
314
+ * file the OS would actually read.
315
+ */
316
+ declare function loadInstructionTree(input: LoadInstructionTreeInput): InstructionTree;
317
+
318
+ /**
319
+ * M74 — compose a base prompt with instruction sources under a character ceiling.
320
+ *
321
+ * ## Why the truncation LADDER is mechanism and the ORDER is policy
322
+ *
323
+ * When the composed text does not fit, something has to go — and which thing goes is a product
324
+ * decision. A coding agent drops repository conventions before user rules; a support agent may do
325
+ * the opposite. Baking that preference into the framework would be absorbing one product's taste as
326
+ * everyone's law, which is the milestone's named risk.
327
+ *
328
+ * So the caller passes `sources` ALREADY ORDERED, most important first, and this function walks that
329
+ * order backwards when it needs room. The framework supplies the cutting mechanism; the product
330
+ * supplies the preference. No source NAME appears in this file.
331
+ *
332
+ * ## Why it truncates rather than refusing
333
+ *
334
+ * A prompt that does not fit is a run that cannot start, and refusing outright would make a long
335
+ * instruction file a hard failure at the worst moment. Dropping the least important source and
336
+ * SAYING SO is the behaviour that keeps the agent usable while keeping the user informed — which is
337
+ * why `onWarn` is not optional in practice even though it is optional in the signature.
338
+ */
339
+ /** One named block of instructions the caller wants composed. */
340
+ interface InstructionSource {
341
+ /** The product's own label, used only in warnings. Never interpreted here. */
342
+ readonly name: string;
343
+ readonly content: string;
344
+ }
345
+ interface ComposeInstructionsOptions {
346
+ /** Ceiling for the whole composed string, base included. */
347
+ readonly maxChars: number;
348
+ /** Where a dropped or trimmed source is reported. Silence here loses the user's instructions. */
349
+ readonly onWarn?: (message: string) => void;
350
+ /** Separator between blocks. */
351
+ readonly separator?: string;
352
+ }
353
+ interface ComposedInstructions {
354
+ readonly text: string;
355
+ /** Names of sources that were dropped entirely, in the order they were dropped. */
356
+ readonly dropped: readonly string[];
357
+ /** Name of the source that was cut in half to fit, when one was. */
358
+ readonly trimmed?: string;
359
+ }
360
+ /**
361
+ * Compose `base` with `sources`, cutting from the END of the list until it fits.
362
+ *
363
+ * The base is never dropped: it is the agent's own identity, and an agent without it is a different
364
+ * agent. If the base alone exceeds the ceiling the result is the base, TRUNCATED, with a warning —
365
+ * returning an empty string would be a silent lobotomy, and throwing would make a long system prompt
366
+ * an unrecoverable configuration error.
367
+ */
368
+ declare function composeInstructions(base: string, sources: readonly InstructionSource[], options: ComposeInstructionsOptions): ComposedInstructions;
369
+
370
+ /**
371
+ * M76 — load custom commands from `.theokit/commands/`.
372
+ *
373
+ * ## Why this gap was worse than a missing feature
374
+ *
375
+ * The framework OWNS the `.theokit/` convention and already loads `skills/`, `agents/` and
376
+ * `hooks.json` from it. `commands/` — the one directory every product-facing agent surface wants —
377
+ * had no loader at all. So a consumer wrote markdown-with-frontmatter scanning **against the
378
+ * framework's own directory**: reimplementing the reading of a convention the framework defines.
379
+ *
380
+ * A convention with a hole in it is worse than no convention. It teaches the reader that `.theokit/`
381
+ * is the framework's, then makes them write the loader themselves for one subdirectory, and their
382
+ * loader inevitably disagrees with ours about frontmatter, precedence and trust.
383
+ *
384
+ * ## Trust, and why project commands are gated
385
+ *
386
+ * A command is a prompt the user can invoke by name. A project-level one comes from the working
387
+ * directory — which, for an agent pointed at a repository the user just cloned, is
388
+ * attacker-controlled content. Same decision as M68: `projectTrusted` false means project commands
389
+ * do not load. User-level commands under `homeDir` need no gate — that is the operator's own
390
+ * machine.
391
+ *
392
+ * ## Warns, never decides
393
+ *
394
+ * On a name collision this loader REPORTS and moves on. Which command wins when a custom one shadows
395
+ * a builtin is the product's router to decide (M83) — it is the only layer that knows what its
396
+ * builtins are. A loader that silently dropped one would make that decision invisibly, on behalf of
397
+ * a product it cannot see.
398
+ */
399
+ /** One command loaded from disk. */
400
+ interface CustomCommand {
401
+ /** Invocation name, derived from the file name without its extension. */
402
+ readonly name: string;
403
+ /** One-line summary from `description:` in the frontmatter, when present. */
404
+ readonly description?: string;
405
+ /** The prompt body, frontmatter removed. */
406
+ readonly body: string;
407
+ /** Which layer it came from. `project` beats `user` — see {@link loadCustomCommands}. */
408
+ readonly source: 'project' | 'user';
409
+ /** Absolute path, for messages a human can act on. */
410
+ readonly path: string;
411
+ }
412
+ interface LoadCustomCommandsInput {
413
+ /** Project root. Its `.theokit/commands/` is read only when `projectTrusted`. */
414
+ readonly projectDir?: string;
415
+ /** Operator home. Its `.theokit/commands/` needs no trust gate. */
416
+ readonly homeDir?: string;
417
+ /**
418
+ * Whether the project directory is trusted (M68/M73).
419
+ *
420
+ * REQUIRED, like `approved` on the hook engine and for the same reason: an optional security gate
421
+ * is a gate somebody forgets, and forgetting this one loads prompts written by whoever wrote the
422
+ * repository.
423
+ */
424
+ readonly projectTrusted: boolean;
425
+ /** Names the product already uses, so a shadow can be reported rather than discovered later. */
426
+ readonly builtinNames?: readonly string[];
427
+ /** Where a shadow, a duplicate, or a malformed file is reported. */
428
+ readonly onWarn?: (message: string) => void;
429
+ }
430
+ interface CustomCommandsResult {
431
+ /** Loaded commands, project-level first. Names are unique — see the precedence note. */
432
+ readonly commands: readonly CustomCommand[];
433
+ /** Names that also exist as builtins. Reported, never resolved here. */
434
+ readonly shadowedBuiltins: readonly string[];
435
+ }
436
+ /**
437
+ * Load `.theokit/commands/*.md` from the project and the user, project winning.
438
+ *
439
+ * Precedence is project-over-user because the project is the more specific scope: a repository that
440
+ * ships a `review` command means *its* review, and having the operator's generic one silently take
441
+ * precedence would make the repository's own configuration the weaker statement.
442
+ */
443
+ declare function loadCustomCommands(input: LoadCustomCommandsInput): CustomCommandsResult;
444
+
445
+ /**
446
+ * Split a markdown file into frontmatter lines and body.
447
+ *
448
+ * ## Why this is shared rather than duplicated
449
+ *
450
+ * Two loaders need it — the M74 instruction tree and the M76 command loader — and they need the SAME
451
+ * answer to the same question: where does the metadata stop and the content begin, and what happens
452
+ * when the fence never closes. That is one piece of knowledge (`G12`), not two similar-looking
453
+ * functions.
454
+ *
455
+ * What is NOT shared is which KEYS each loader reads. `paths:` matters to instructions and
456
+ * `description:` matters to commands, and folding those together would build a vocabulary neither
457
+ * one asked for.
458
+ */
459
+ interface ParsedFrontmatter {
460
+ /** Lines between the fences, excluding them. Empty when the file has no frontmatter. */
461
+ readonly frontmatter: readonly string[];
462
+ /** Everything after the closing fence — or the whole file when there is no frontmatter. */
463
+ readonly body: string;
464
+ }
465
+ /**
466
+ * Split `raw`, or return `undefined` when the frontmatter opens and never closes.
467
+ *
468
+ * `undefined` rather than a best guess: a file whose fence never closes is malformed in a way that
469
+ * makes its metadata unknowable, and guessing whether the rest is body or metadata feeds the caller
470
+ * either the wrong text or the wrong settings. Both callers treat that as "skip this file, warn,
471
+ * keep going" — failure is per file, never per tree.
472
+ */
473
+ declare function splitFrontmatter(raw: string): ParsedFrontmatter | undefined;
474
+ /**
475
+ * Read one scalar key from frontmatter lines.
476
+ *
477
+ * Deliberately not a YAML parser. Both callers read a handful of known keys, and pulling in a parser
478
+ * to do that would be a dependency for a feature nobody asked for (parsimony rungs 4 → 1). An
479
+ * unrecognised key is ignored rather than rejected — a product may put its own metadata there.
480
+ */
481
+ declare function frontmatterValue(frontmatter: readonly string[], key: string): string | undefined;
482
+
483
+ /**
484
+ * M74 — context pressure: the fraction of the window a run has consumed.
485
+ *
486
+ * ## Why this had no counterpart
487
+ *
488
+ * The framework shipped the DENOMINATOR (`resolveEffectiveContextWindow`, crossed in M67) and the
489
+ * NUMERATOR (token usage, on every `done` event) and never put them together. So every product that
490
+ * wanted to warn a user before a run hit the wall computed the ratio itself, and each picked its own
491
+ * thresholds.
492
+ *
493
+ * The arithmetic is trivial; publishing it is not about saving a division. It is about there being
494
+ * ONE answer to "is this run in trouble", so a warning in the CLI and a badge in a dashboard agree.
495
+ */
496
+ /** How close a run is to its context limit. */
497
+ type ContextPressure = 'ok' | 'warn' | 'critical';
498
+ /**
499
+ * Fractions of the effective window at which each level begins.
500
+ *
501
+ * Defaults chosen to leave room to act: `warn` at 75% is early enough that compacting still helps,
502
+ * `critical` at 90% is where the next turn may not fit. Configurable because a product with long
503
+ * tool outputs legitimately wants an earlier warning than one with short chat turns.
504
+ */
505
+ interface ContextPressureThresholds {
506
+ readonly warn: number;
507
+ readonly critical: number;
508
+ }
509
+ declare const DEFAULT_CONTEXT_PRESSURE_THRESHOLDS: ContextPressureThresholds;
510
+ /** Raised when thresholds are ordered in a way that makes a level unreachable. */
511
+ declare class ContextPressureThresholdError extends TheokitAgentError {
512
+ readonly name = "ContextPressureThresholdError";
513
+ constructor(message: string);
514
+ }
515
+ /**
516
+ * Classify a run's context pressure.
517
+ *
518
+ * @param usedTokens tokens the run has consumed
519
+ * @param effectiveWindow the window it is consuming from — typically
520
+ * `resolveEffectiveContextWindow(...)`, re-exported here so a caller reaches both through one import
521
+ * (see {@link effectiveContextWindow}).
522
+ *
523
+ * An `effectiveWindow` of zero or less returns `'ok'` rather than dividing: an unknown window is not
524
+ * evidence of pressure, and `Infinity`/`NaN` reaching a UI as a percentage is worse than saying
525
+ * nothing. Missing evidence is not evidence — the same rule the transcript collector applies to a
526
+ * missing mtime.
527
+ *
528
+ * @throws {ContextPressureThresholdError} when `warn` is not below `critical` — a caller who
529
+ * inverted them holds a belief about their own thresholds that is wrong, and silently sorting them
530
+ * would leave that belief intact.
531
+ */
532
+ declare function contextPressure(usedTokens: number, effectiveWindow: number, thresholds?: ContextPressureThresholds): ContextPressure;
533
+
534
+ export { type ComposeInstructionsOptions, type ComposedInstructions, type ConfigLayer, type ContextPressure, ContextPressureThresholdError, type ContextPressureThresholds, type CustomCommand, type CustomCommandsResult, DEFAULT_CONTEXT_PRESSURE_THRESHOLDS, type ExpandImportsInput, type InstructionBlock, type InstructionSource, type InstructionTree, type InstructionTreeBudget, LayerOutOfOrderError, LayeredConfig, type LayeredConfigInput, type LayeredConfigResult, type LoadCustomCommandsInput, type LoadInstructionTreeInput, type ParsedFrontmatter, type PrecedenceReport, type ProvenancePerKey, type TrustRecord, TrustStore, TrustStorePermissionsError, composeInstructions, contextPressure, expandInstructionImports, frontmatterValue, loadCustomCommands, loadInstructionTree, splitFrontmatter };