@fastpaca/cria 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fastpaca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ <h1 align="center">Cria</h1>
2
+
3
+ <p align="center">
4
+ Cria is a tiny library for building LLM prompts as reusable components.
5
+ </p>
6
+
7
+ <p align="center">
8
+ <i>Debug, view, and save your prompts easily. Swap out components without major rewrites and test your prompts.</i>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@fastpaca/cria"><img src="https://img.shields.io/npm/v/@fastpaca/cria?logo=npm&logoColor=white" alt="npm"></a>
13
+ <a href="https://opensource.org/license/mit"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://github.com/fastpaca/cria/stargazers">
18
+ <img src="https://img.shields.io/badge/Give%20a%20Star-Support%20the%20project-orange?style=for-the-badge" alt="Give a Star">
19
+ </a>
20
+ </p>
21
+
22
+ Most prompt construction is string concatenation. Append everything to some buffer, hope the important parts survive when you hit limits or want to save cost.
23
+
24
+ Cria lets you declare what's expendable and what is not, and makes your prompts failure mode explicit.
25
+
26
+ Cria treats memory layout as a first-class concern. You declare priorities upfront, and the library handles eviction when needed. Components let you test retrieval logic separately from system prompts, swap implementations without rewrites, and debug exactly which content got cut when quality degrades.
27
+
28
+ ```tsx
29
+ const prompt = (
30
+ <Region priority={0}>
31
+ You are a helpful assistant.
32
+
33
+ {/* Only preserve 80k tokens of history */}
34
+ <Truncate budget={80000} priority={2}>
35
+ {conversationHistory}
36
+ </Truncate>
37
+
38
+ {/* Only preserve 20k tokens of tool calls. It gets dropped
39
+ first in case we need to. */}
40
+ <Truncate budget={20000} priority={3}>
41
+ {toolCalls}
42
+ </Truncate>
43
+
44
+ {/* Skip examples in case we are bad on budget */}
45
+ <Omit priority={3}>{examples}</Omit>
46
+
47
+ {userMessage}
48
+ </Region>
49
+ );
50
+
51
+ render(prompt, { tokenizer, budget: 128000 });
52
+ ```
53
+
54
+ Cria will drop lower priority sections or truncate them in case it hits your prompt limits.
55
+ ## Features
56
+
57
+ - **Composable** — Build prompts from reusable components. Test and optimize each part independently.
58
+ - **Priority-based** — Declare what's sacred (priority 0) and what's expendable (priority 3). No more guessing what gets cut.
59
+ - **Flexible strategies** — Truncate content progressively, omit entire sections, or write custom eviction logic.
60
+ - **Tiny** — Zero dependencies.
61
+
62
+ ## Getting Started
63
+
64
+ ```bash
65
+ npm install @fastpaca/cria
66
+ ```
67
+
68
+ Add to your `tsconfig.json`:
69
+
70
+ ```json
71
+ {
72
+ "compilerOptions": {
73
+ "jsx": "react-jsx",
74
+ "jsxImportSource": "@fastpaca/cria"
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## Documentation
80
+
81
+ ### Components
82
+
83
+ **`<Region>`** — The basic building block. Groups content with a priority level.
84
+
85
+ ```jsx
86
+ <Region>
87
+ <Region priority={0}>System instructions</Region>
88
+ <Region priority={2}>Retrieved context</Region>
89
+ </Region>
90
+ ```
91
+
92
+ **`<Truncate>`** — Progressively shortens content when over budget.
93
+
94
+ ```jsx
95
+ <Truncate budget={10000} from="start" priority={2}>
96
+ {longConversation}
97
+ </Truncate>
98
+ ```
99
+
100
+ **`<Omit>`** — Drops entirely when space is needed.
101
+
102
+ ```jsx
103
+ <Omit priority={3}>{optionalExamples}</Omit>
104
+ ```
105
+
106
+ ### Priority Levels
107
+
108
+ Lower number = higher importance.
109
+
110
+ | Priority | Use for |
111
+ |----------|---------|
112
+ | 0 | System prompt, safety rules |
113
+ | 1 | Current user message, tool outputs |
114
+ | 2 | Conversation history, retrieved docs |
115
+ | 3 | Examples, optional context |
116
+
117
+ ### Tokenizer
118
+
119
+ Pass any function that counts tokens:
120
+
121
+ ```tsx
122
+ import { encoding_for_model } from "tiktoken";
123
+
124
+ const enc = encoding_for_model("gpt-4");
125
+ const tokenizer = (text: string) => enc.encode(text).length;
126
+
127
+ render(prompt, { tokenizer, budget: 128000 });
128
+ ```
129
+
130
+ ### Custom Strategies
131
+
132
+ Write your own eviction logic:
133
+
134
+ ```tsx
135
+ import type { Strategy } from "@fastpaca/cria";
136
+
137
+ const summarize: Strategy = ({ target, tokenizer }) => {
138
+ const summary = createSummary(target.content);
139
+ return [{ ...target, content: summary, tokens: tokenizer(summary) }];
140
+ };
141
+
142
+ <Region priority={2} strategy={summarize}>{document}</Region>
143
+ ```
144
+
145
+ ### Error Handling
146
+
147
+ ```tsx
148
+ import { FitError } from "@fastpaca/cria";
149
+
150
+ try {
151
+ render(prompt, { tokenizer, budget: 1000 });
152
+ } catch (e) {
153
+ if (e instanceof FitError) {
154
+ console.log(`Over budget by ${e.overBudgetBy} tokens`);
155
+ }
156
+ }
157
+ ```
158
+
159
+ ## Contributing
160
+
161
+ Contributions are welcome! Please feel free to submit a Pull Request.
162
+
163
+ ## License
164
+
165
+ MIT © [Fastpaca](https://fastpaca.com)
@@ -0,0 +1,78 @@
1
+ import type { PromptChildren, PromptElement, Strategy } from "./types";
2
+ /** Props for the Region component. */
3
+ interface RegionProps {
4
+ /** Lower number = higher importance. Default: 0 (highest priority) */
5
+ priority?: number;
6
+ /** Optional strategy to apply when this region needs to shrink */
7
+ strategy?: Strategy;
8
+ /** Stable identifier for caching/debugging */
9
+ id?: string;
10
+ /** Content of this region */
11
+ children?: PromptChildren;
12
+ }
13
+ /**
14
+ * The fundamental building block of Cria prompts—think of it as `<div>`.
15
+ *
16
+ * Regions define sections of your prompt with a priority level. During fitting,
17
+ * regions with higher priority numbers (less important) are reduced first.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <Region priority={0}>You are a helpful assistant.</Region>
22
+ * <Region priority={2}>{documents}</Region>
23
+ * <Region priority={1}>{userMessage}</Region>
24
+ * ```
25
+ */
26
+ export declare function Region({ priority, strategy, id, children, }: RegionProps): PromptElement;
27
+ /** Props for the Truncate component. */
28
+ interface TruncateProps {
29
+ /** Maximum token count for this region's content */
30
+ budget: number;
31
+ /** Which end to truncate from. Default: "start" */
32
+ from?: "start" | "end";
33
+ /** Lower number = higher importance. Default: 0 */
34
+ priority?: number;
35
+ /** Stable identifier for caching/debugging */
36
+ id?: string;
37
+ /** Content to truncate */
38
+ children?: PromptChildren;
39
+ }
40
+ /**
41
+ * A region that truncates its content to fit within a token budget.
42
+ *
43
+ * When the overall prompt exceeds budget, Truncate regions progressively
44
+ * remove content from the specified direction until they meet their budget.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <Truncate budget={20000} priority={2}>
49
+ * {conversationHistory}
50
+ * </Truncate>
51
+ * ```
52
+ */
53
+ export declare function Truncate({ budget, from, priority, id, children, }: TruncateProps): PromptElement;
54
+ /** Props for the Omit component. */
55
+ interface OmitProps {
56
+ /** Lower number = higher importance. Default: 0 */
57
+ priority?: number;
58
+ /** Stable identifier for caching/debugging */
59
+ id?: string;
60
+ /** Content that may be omitted */
61
+ children?: PromptChildren;
62
+ }
63
+ /**
64
+ * A region that is entirely removed when the prompt needs to shrink.
65
+ *
66
+ * Use Omit for "nice to have" content that can be dropped entirely if needed.
67
+ * When the prompt exceeds budget, Omit regions are removed (lowest priority first).
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * <Omit priority={3}>
72
+ * {optionalExamples}
73
+ * </Omit>
74
+ * ```
75
+ */
76
+ export declare function Omit({ priority, id, children, }: OmitProps): PromptElement;
77
+ export {};
78
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEb,QAAQ,EAET,MAAM,SAAS,CAAC;AAEjB,sCAAsC;AACtC,UAAU,WAAW;IACnB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,8CAA8C;IAC9C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,EACrB,QAAY,EACZ,QAAQ,EACR,EAAE,EACF,QAAa,GACd,EAAE,WAAW,GAAG,aAAa,CAO7B;AAED,wCAAwC;AACxC,UAAU,aAAa;IACrB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,EACvB,MAAM,EACN,IAAc,EACd,QAAY,EACZ,EAAE,EACF,QAAa,GACd,EAAE,aAAa,GAAG,aAAa,CA4C/B;AAED,oCAAoC;AACpC,UAAU,SAAS;IACjB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,EACnB,QAAY,EACZ,EAAE,EACF,QAAa,GACd,EAAE,SAAS,GAAG,aAAa,CAS3B"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The fundamental building block of Cria prompts—think of it as `<div>`.
3
+ *
4
+ * Regions define sections of your prompt with a priority level. During fitting,
5
+ * regions with higher priority numbers (less important) are reduced first.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <Region priority={0}>You are a helpful assistant.</Region>
10
+ * <Region priority={2}>{documents}</Region>
11
+ * <Region priority={1}>{userMessage}</Region>
12
+ * ```
13
+ */
14
+ export function Region({ priority = 0, strategy, id, children = [], }) {
15
+ return {
16
+ priority,
17
+ children: children,
18
+ ...(strategy && { strategy }),
19
+ ...(id && { id }),
20
+ };
21
+ }
22
+ /**
23
+ * A region that truncates its content to fit within a token budget.
24
+ *
25
+ * When the overall prompt exceeds budget, Truncate regions progressively
26
+ * remove content from the specified direction until they meet their budget.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <Truncate budget={20000} priority={2}>
31
+ * {conversationHistory}
32
+ * </Truncate>
33
+ * ```
34
+ */
35
+ export function Truncate({ budget, from = "start", priority = 0, id, children = [], }) {
36
+ const strategy = (input) => {
37
+ const { target, tokenizer } = input;
38
+ if (target.tokens <= budget) {
39
+ return [target];
40
+ }
41
+ let content = target.content;
42
+ let tokens = target.tokens;
43
+ // TODO(v1): Optimize - this calls tokenizer O(n) times. Consider:
44
+ // - Estimate chars/token ratio, binary search to target
45
+ // - Cache intermediate token counts
46
+ while (tokens > budget && content.length > 0) {
47
+ const charsToRemove = Math.max(1, Math.floor(content.length * 0.1));
48
+ if (from === "start") {
49
+ content = content.slice(charsToRemove);
50
+ }
51
+ else {
52
+ content = content.slice(0, -charsToRemove);
53
+ }
54
+ tokens = tokenizer(content);
55
+ }
56
+ if (content.length === 0) {
57
+ return [];
58
+ }
59
+ return [
60
+ {
61
+ content,
62
+ tokens,
63
+ priority: target.priority,
64
+ regionId: target.regionId,
65
+ index: target.index,
66
+ },
67
+ ];
68
+ };
69
+ return {
70
+ priority,
71
+ children: children,
72
+ strategy,
73
+ ...(id && { id }),
74
+ };
75
+ }
76
+ /**
77
+ * A region that is entirely removed when the prompt needs to shrink.
78
+ *
79
+ * Use Omit for "nice to have" content that can be dropped entirely if needed.
80
+ * When the prompt exceeds budget, Omit regions are removed (lowest priority first).
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * <Omit priority={3}>
85
+ * {optionalExamples}
86
+ * </Omit>
87
+ * ```
88
+ */
89
+ export function Omit({ priority = 0, id, children = [], }) {
90
+ const strategy = () => [];
91
+ return {
92
+ priority,
93
+ children: children,
94
+ strategy,
95
+ ...(id && { id }),
96
+ };
97
+ }
98
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.js","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,MAAM,CAAC,EACrB,QAAQ,GAAG,CAAC,EACZ,QAAQ,EACR,EAAE,EACF,QAAQ,GAAG,EAAE,GACD;IACZ,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,QAAsC;QAChD,GAAG,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC7B,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;KAClB,CAAC;AACJ,CAAC;AAgBD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,MAAM,EACN,IAAI,GAAG,OAAO,EACd,QAAQ,GAAG,CAAC,EACZ,EAAE,EACF,QAAQ,GAAG,EAAE,GACC;IACd,MAAM,QAAQ,GAAa,CAAC,KAAoB,EAAoB,EAAE;QACpE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;QACpC,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;YAC5B,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC7B,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAE3B,kEAAkE;QAClE,wDAAwD;QACxD,oCAAoC;QACpC,OAAO,MAAM,GAAG,MAAM,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC;YACpE,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACrB,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YACzC,CAAC;iBAAM,CAAC;gBACN,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC;YAC7C,CAAC;YACD,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL;gBACE,OAAO;gBACP,MAAM;gBACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,KAAK,EAAE,MAAM,CAAC,KAAK;aACpB;SACF,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,QAAsC;QAChD,QAAQ;QACR,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;KAClB,CAAC;AACJ,CAAC;AAYD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,IAAI,CAAC,EACnB,QAAQ,GAAG,CAAC,EACZ,EAAE,EACF,QAAQ,GAAG,EAAE,GACH;IACV,MAAM,QAAQ,GAAa,GAAqB,EAAE,CAAC,EAAE,CAAC;IAEtD,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,QAAsC;QAChD,QAAQ;QACR,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;KAClB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cria - JSX-based prompt renderer with automatic token budget fitting.
3
+ *
4
+ * @example
5
+ * ```tsx
6
+ * import { render, Region, Truncate, Omit } from "@fastpaca/cria";
7
+ *
8
+ * const prompt = (
9
+ * <Region priority={0}>
10
+ * You are a helpful assistant.
11
+ * <Truncate budget={20000} direction="start" priority={2}>
12
+ * {conversationHistory}
13
+ * </Truncate>
14
+ * <Omit priority={3}>
15
+ * {optionalContext}
16
+ * </Omit>
17
+ * </Region>
18
+ * );
19
+ *
20
+ * const result = render(prompt, { tokenizer, budget: 128000 });
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ export { Omit, Region, Truncate } from "./components";
26
+ export { render } from "./render";
27
+ export type { PromptChildren, PromptElement, PromptFragment, Strategy, StrategyInput, Tokenizer, } from "./types";
28
+ export { FitError } from "./types";
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EACV,cAAc,EACd,aAAa,EACb,cAAc,EACd,QAAQ,EACR,aAAa,EACb,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cria - JSX-based prompt renderer with automatic token budget fitting.
3
+ *
4
+ * @example
5
+ * ```tsx
6
+ * import { render, Region, Truncate, Omit } from "@fastpaca/cria";
7
+ *
8
+ * const prompt = (
9
+ * <Region priority={0}>
10
+ * You are a helpful assistant.
11
+ * <Truncate budget={20000} direction="start" priority={2}>
12
+ * {conversationHistory}
13
+ * </Truncate>
14
+ * <Omit priority={3}>
15
+ * {optionalContext}
16
+ * </Omit>
17
+ * </Region>
18
+ * );
19
+ *
20
+ * const result = render(prompt, { tokenizer, budget: 128000 });
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ // biome-ignore lint/performance/noBarrelFile: Entry point for package exports
26
+ export { Omit, Region, Truncate } from "./components";
27
+ export { render } from "./render";
28
+ export { FitError } from "./types";
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,8EAA8E;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AASlC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { PromptElement } from "./types";
2
+ type ComponentFn = (props: Props) => PromptElement;
3
+ type Child = PromptElement | string | number | boolean | null | undefined | Child[];
4
+ type Props = Record<string, unknown> & {
5
+ children?: Child | Child[];
6
+ };
7
+ export declare const Fragment: unique symbol;
8
+ export declare function jsx(type: ComponentFn | typeof Fragment, props: Props): PromptElement;
9
+ export declare function jsxs(type: ComponentFn | typeof Fragment, props: Props): PromptElement;
10
+ export declare namespace JSX {
11
+ type Element = PromptElement;
12
+ type IntrinsicElements = {};
13
+ interface ElementChildrenAttribute {
14
+ children: unknown;
15
+ }
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=jsx-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-runtime.d.ts","sourceRoot":"","sources":["../src/jsx-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC;AACnD,KAAK,KAAK,GACN,aAAa,GACb,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,KAAK,EAAE,CAAC;AACZ,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE,CAAA;CAAE,CAAC;AAgCtE,eAAO,MAAM,QAAQ,eAA8B,CAAC;AAGpD,wBAAgB,GAAG,CACjB,IAAI,EAAE,WAAW,GAAG,OAAO,QAAQ,EACnC,KAAK,EAAE,KAAK,GACX,aAAa,CASf;AAGD,wBAAgB,IAAI,CAClB,IAAI,EAAE,WAAW,GAAG,OAAO,QAAQ,EACnC,KAAK,EAAE,KAAK,GACX,aAAa,CAEf;AAGD,yBAAiB,GAAG,CAAC;IACnB,KAAY,OAAO,GAAG,aAAa,CAAC;IAEpC,KAAY,iBAAiB,GAAG,EAAE,CAAC;IACnC,UAAiB,wBAAwB;QACvC,QAAQ,EAAE,OAAO,CAAC;KACnB;CACF"}
@@ -0,0 +1,40 @@
1
+ // Normalize children: flatten arrays, filter nullish, coerce numbers to strings
2
+ function normalizeChildren(children) {
3
+ if (children === undefined || children === null) {
4
+ return [];
5
+ }
6
+ if (typeof children === "boolean") {
7
+ return [];
8
+ }
9
+ if (typeof children === "string") {
10
+ return [children];
11
+ }
12
+ if (typeof children === "number") {
13
+ return [String(children)];
14
+ }
15
+ if (Array.isArray(children)) {
16
+ const result = [];
17
+ for (const child of children) {
18
+ result.push(...normalizeChildren(child));
19
+ }
20
+ return result;
21
+ }
22
+ // It's a PromptElement
23
+ return [children];
24
+ }
25
+ // Fragment: just returns children (inlined into parent)
26
+ export const Fragment = Symbol.for("cria.fragment");
27
+ // jsx: called by TypeScript for single child
28
+ export function jsx(type, props) {
29
+ const children = normalizeChildren(props.children);
30
+ if (type === Fragment) {
31
+ // Fragment returns a wrapper element that just holds children
32
+ return { priority: 0, children };
33
+ }
34
+ return type({ ...props, children });
35
+ }
36
+ // jsxs: called by TypeScript for multiple children (same behavior)
37
+ export function jsxs(type, props) {
38
+ return jsx(type, props);
39
+ }
40
+ //# sourceMappingURL=jsx-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-runtime.js","sourceRoot":"","sources":["../src/jsx-runtime.ts"],"names":[],"mappings":"AAaA,gFAAgF;AAChF,SAAS,iBAAiB,CACxB,QAAqC;IAErC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAA+B,EAAE,CAAC;QAC9C,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,uBAAuB;IACvB,OAAO,CAAC,QAAQ,CAAC,CAAC;AACpB,CAAC;AAED,wDAAwD;AACxD,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAEpD,6CAA6C;AAC7C,MAAM,UAAU,GAAG,CACjB,IAAmC,EACnC,KAAY;IAEZ,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,8DAA8D;QAC9D,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC;IACnC,CAAC;IAED,OAAO,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,IAAI,CAClB,IAAmC,EACnC,KAAY;IAEZ,OAAO,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { type PromptElement, type Tokenizer } from "./types";
2
+ /** Options for the render function. */
3
+ interface RenderOptions {
4
+ /** Function to count tokens in a string (e.g., tiktoken) */
5
+ tokenizer: Tokenizer;
6
+ /** Maximum token count for the final output */
7
+ budget: number;
8
+ }
9
+ /**
10
+ * Renders a PromptElement tree to a fitted string.
11
+ *
12
+ * This is the main entry point for Cria. Takes a JSX tree and returns a string
13
+ * that fits within the specified token budget.
14
+ *
15
+ * **Pipeline:**
16
+ * 1. `flatten`: Walks the tree, collects text into ordered PromptFragment[]
17
+ * 2. `fitToBudget`: Applies strategies starting from lowest priority until under budget
18
+ * 3. `join`: Concatenates fragment content into final string
19
+ *
20
+ * @param element - The root PromptElement (from JSX)
21
+ * @param options - Tokenizer and budget configuration
22
+ * @returns The fitted prompt string
23
+ * @throws {FitError} When the prompt cannot fit within budget
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { render, Region, Omit } from "@fastpaca/cria";
28
+ *
29
+ * const prompt = (
30
+ * <Region priority={0}>
31
+ * System prompt
32
+ * <Omit priority={2}>Optional context</Omit>
33
+ * </Region>
34
+ * );
35
+ *
36
+ * const result = render(prompt, {
37
+ * tokenizer: (text) => Math.ceil(text.length / 4),
38
+ * budget: 1000,
39
+ * });
40
+ * ```
41
+ */
42
+ export declare function render(element: PromptElement, { tokenizer, budget }: RenderOptions): string;
43
+ export {};
44
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAIlB,KAAK,SAAS,EACf,MAAM,SAAS,CAAC;AAEjB,uCAAuC;AACvC,UAAU,aAAa;IACrB,4DAA4D;IAC5D,SAAS,EAAE,SAAS,CAAC;IACrB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,aAAa,EACtB,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,GACnC,MAAM,CAQR"}
package/dist/render.js ADDED
@@ -0,0 +1,171 @@
1
+ import { FitError, } from "./types";
2
+ /**
3
+ * Renders a PromptElement tree to a fitted string.
4
+ *
5
+ * This is the main entry point for Cria. Takes a JSX tree and returns a string
6
+ * that fits within the specified token budget.
7
+ *
8
+ * **Pipeline:**
9
+ * 1. `flatten`: Walks the tree, collects text into ordered PromptFragment[]
10
+ * 2. `fitToBudget`: Applies strategies starting from lowest priority until under budget
11
+ * 3. `join`: Concatenates fragment content into final string
12
+ *
13
+ * @param element - The root PromptElement (from JSX)
14
+ * @param options - Tokenizer and budget configuration
15
+ * @returns The fitted prompt string
16
+ * @throws {FitError} When the prompt cannot fit within budget
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { render, Region, Omit } from "@fastpaca/cria";
21
+ *
22
+ * const prompt = (
23
+ * <Region priority={0}>
24
+ * System prompt
25
+ * <Omit priority={2}>Optional context</Omit>
26
+ * </Region>
27
+ * );
28
+ *
29
+ * const result = render(prompt, {
30
+ * tokenizer: (text) => Math.ceil(text.length / 4),
31
+ * budget: 1000,
32
+ * });
33
+ * ```
34
+ */
35
+ export function render(element, { tokenizer, budget }) {
36
+ if (budget <= 0) {
37
+ return "";
38
+ }
39
+ const fragments = flatten(element, tokenizer, { counter: 0 });
40
+ const fitted = fitToBudget(fragments, budget, tokenizer);
41
+ return fitted.map((f) => f.content).join("");
42
+ }
43
+ /**
44
+ * Turns a PromptElement tree into an ordered list of PromptFragment.
45
+ *
46
+ * - Preserves text order (flush text buffer before descending into child elements).
47
+ * - Inherits priority/strategy from the emitting element.
48
+ * - Assigns regionId: explicit id if provided, else auto-incrementing counter.
49
+ * - Computes token counts with the provided tokenizer.
50
+ */
51
+ function flatten(element, tokenizer, ctx, fragments = []) {
52
+ let buffer = "";
53
+ const flushBuffer = () => {
54
+ if (buffer.length === 0) {
55
+ return;
56
+ }
57
+ const tokens = tokenizer(buffer);
58
+ if (tokens > 0) {
59
+ const fragment = {
60
+ content: buffer,
61
+ tokens,
62
+ priority: element.priority,
63
+ regionId: element.id ?? `r${ctx.counter++}`,
64
+ index: fragments.length,
65
+ };
66
+ if (element.strategy) {
67
+ fragment.strategy = element.strategy;
68
+ }
69
+ fragments.push(fragment);
70
+ }
71
+ buffer = "";
72
+ };
73
+ for (const child of element.children) {
74
+ if (!child) {
75
+ continue;
76
+ }
77
+ if (typeof child === "string") {
78
+ buffer += child;
79
+ }
80
+ else {
81
+ // Flush current text before descending to maintain order
82
+ flushBuffer();
83
+ flatten(child, tokenizer, ctx, fragments);
84
+ }
85
+ }
86
+ // Flush any trailing text
87
+ flushBuffer();
88
+ return fragments;
89
+ }
90
+ /**
91
+ * Finds the highest priority number (least important) among fragments with strategies.
92
+ */
93
+ function findLowestImportancePriority(fragments) {
94
+ const priorities = fragments
95
+ .filter((f) => f.strategy !== undefined)
96
+ .map((f) => f.priority);
97
+ if (priorities.length === 0) {
98
+ return null;
99
+ }
100
+ return Math.max(...priorities);
101
+ }
102
+ /**
103
+ * Applies a single strategy to its target fragment.
104
+ * Splices replacements in-place and recomputes token counts.
105
+ */
106
+ function applyStrategy(result, target, budget, tokenizer, iteration) {
107
+ const strategy = target.strategy;
108
+ const targetIndex = result.findIndex((f) => f.regionId === target.regionId);
109
+ if (targetIndex === -1) {
110
+ return;
111
+ }
112
+ const currentTarget = result[targetIndex];
113
+ if (!currentTarget) {
114
+ return;
115
+ }
116
+ const input = {
117
+ fragments: result,
118
+ target: currentTarget,
119
+ budget,
120
+ tokenizer,
121
+ totalTokens: result.reduce((sum, f) => sum + f.tokens, 0),
122
+ iteration,
123
+ };
124
+ const replacement = strategy(input);
125
+ // Splice replacement at target position
126
+ result.splice(targetIndex, 1, ...replacement);
127
+ // Recompute tokens for modified fragments
128
+ for (const frag of replacement) {
129
+ frag.tokens = tokenizer(frag.content);
130
+ }
131
+ }
132
+ /**
133
+ * Repeatedly applies strategies starting from the least-important priority
134
+ * until the total token count is within budget.
135
+ *
136
+ * Throws FitError if:
137
+ * - No progress is made in an iteration (strategies didn't reduce tokens)
138
+ * - No strategies remain but still over budget
139
+ */
140
+ function fitToBudget(fragments, budget, tokenizer) {
141
+ const result = [...fragments];
142
+ let iteration = 0;
143
+ const maxIterations = 1000;
144
+ while (true) {
145
+ const totalTokens = result.reduce((sum, f) => sum + f.tokens, 0);
146
+ if (totalTokens <= budget) {
147
+ return result;
148
+ }
149
+ iteration++;
150
+ if (iteration > maxIterations) {
151
+ throw new FitError(totalTokens - budget, -1, iteration);
152
+ }
153
+ const lowestImportancePriority = findLowestImportancePriority(result);
154
+ if (lowestImportancePriority === null) {
155
+ throw new FitError(totalTokens - budget, -1, iteration);
156
+ }
157
+ // Collect all targets at this priority
158
+ const targets = result.filter((f) => f.strategy !== undefined && f.priority === lowestImportancePriority);
159
+ const tokensBefore = totalTokens;
160
+ // Apply strategies in stable order
161
+ for (const target of targets) {
162
+ applyStrategy(result, target, budget, tokenizer, iteration);
163
+ }
164
+ // Check progress
165
+ const tokensAfter = result.reduce((sum, f) => sum + f.tokens, 0);
166
+ if (tokensAfter >= tokensBefore) {
167
+ throw new FitError(tokensAfter - budget, lowestImportancePriority, iteration);
168
+ }
169
+ }
170
+ }
171
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=render.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.test.d.ts","sourceRoot":"","sources":["../src/render.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@fastpaca/cria/jsx-runtime";
2
+ import assert from "node:assert/strict";
3
+ import { test } from "node:test";
4
+ import { Omit, Region, render, Truncate } from "./index";
5
+ // Simple tokenizer: 1 token per 4 characters (approximates real tokenizers)
6
+ const tokenizer = (text) => Math.ceil(text.length / 4);
7
+ const FIT_ERROR_PATTERN = /Cannot fit prompt/;
8
+ test("render: basic text output", () => {
9
+ const element = _jsx(Region, { priority: 0, children: "Hello, world!" });
10
+ const result = render(element, { tokenizer, budget: 100 });
11
+ assert.strictEqual(result, "Hello, world!");
12
+ });
13
+ test("render: nested regions", () => {
14
+ const element = (_jsxs(Region, { priority: 0, children: ["Start ", _jsx(Region, { priority: 1, children: "Middle" }), " End"] }));
15
+ const result = render(element, { tokenizer, budget: 100 });
16
+ assert.strictEqual(result, "Start Middle End");
17
+ });
18
+ test("render: omit removes region when over budget", () => {
19
+ const element = (_jsxs(Region, { priority: 0, children: ["Important", " ", _jsx(Omit, { priority: 1, children: "Less important content that should be removed" }), "Also important"] }));
20
+ const resultLarge = render(element, { tokenizer, budget: 100 });
21
+ assert.ok(resultLarge.includes("Less important"));
22
+ const resultSmall = render(element, { tokenizer, budget: 10 });
23
+ assert.ok(!resultSmall.includes("Less important"));
24
+ assert.ok(resultSmall.includes("Important"));
25
+ });
26
+ test("render: truncate reduces content", () => {
27
+ const longContent = "A".repeat(100);
28
+ const element = (_jsxs(Region, { priority: 0, children: ["Header", " ", _jsx(Truncate, { budget: 5, priority: 1, children: longContent })] }));
29
+ const result = render(element, { tokenizer, budget: 10 });
30
+ assert.ok(result.length < 100);
31
+ assert.ok(result.includes("Header"));
32
+ });
33
+ test("render: priority ordering - lower priority removed first", () => {
34
+ const element = (_jsxs(Region, { priority: 0, children: [_jsx(Region, { priority: 0, children: "Critical" }), _jsx(Omit, { priority: 1, children: "Medium importance" }), _jsx(Omit, { priority: 2, children: "Low importance" })] }));
35
+ const result = render(element, { tokenizer, budget: 5 });
36
+ assert.ok(result.includes("Critical"));
37
+ assert.ok(!result.includes("Medium"));
38
+ assert.ok(!result.includes("Low"));
39
+ });
40
+ test("render: throws FitError when cannot fit", () => {
41
+ const element = (_jsx(Region, { priority: 0, children: "This content has no strategy and cannot be reduced" }));
42
+ assert.throws(() => render(element, { tokenizer, budget: 1 }), FIT_ERROR_PATTERN);
43
+ });
44
+ test("render: multiple strategies at same priority applied together", () => {
45
+ const element = (_jsxs(Region, { priority: 0, children: [_jsx(Omit, { id: "a", priority: 1, children: "AAA" }), _jsx(Omit, { id: "b", priority: 1, children: "BBB" })] }));
46
+ const result = render(element, { tokenizer, budget: 0 });
47
+ assert.strictEqual(result, "");
48
+ });
49
+ //# sourceMappingURL=render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.test.js","sourceRoot":"","sources":["../src/render.test.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * What can be passed as children to a Cria component.
3
+ *
4
+ * Includes all JSX-compatible values: elements, strings, numbers, booleans,
5
+ * null/undefined (ignored), and arrays (flattened). The jsx-runtime normalizes
6
+ * these into `(PromptElement | string)[]` before storing in the element.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * <Region>
11
+ * {"Hello"}
12
+ * {123}
13
+ * {items.map(item => <Region>{item}</Region>)}
14
+ * </Region>
15
+ * ```
16
+ */
17
+ export type PromptChildren = PromptElement | string | number | boolean | null | undefined | readonly PromptChildren[];
18
+ /**
19
+ * The core IR node type. All Cria components return a PromptElement.
20
+ *
21
+ * This is the normalized representation after JSX transformation.
22
+ * The render pipeline traverses this tree to produce fragments for fitting.
23
+ *
24
+ * @property priority - Lower number = higher importance (kept longer during fitting)
25
+ * @property strategy - Optional function to reduce this region when over budget
26
+ * @property id - Optional stable identifier for caching/debugging
27
+ * @property children - Normalized array of child elements and text
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * // Created via JSX:
32
+ * <Region priority={0}>System prompt</Region>
33
+ *
34
+ * // Produces:
35
+ * { priority: 0, children: ["System prompt"] }
36
+ * ```
37
+ */
38
+ export interface PromptElement {
39
+ priority: number;
40
+ strategy?: Strategy;
41
+ id?: string;
42
+ children: (PromptElement | string)[];
43
+ }
44
+ /**
45
+ * A flattened text fragment produced by the render pipeline.
46
+ *
47
+ * The render step walks the PromptElement tree and emits an ordered list of
48
+ * fragments. The fit loop then applies strategies to reduce token count.
49
+ *
50
+ * @property content - The text content of this fragment
51
+ * @property tokens - Token count (computed via the provided tokenizer)
52
+ * @property priority - Inherited from the emitting element
53
+ * @property regionId - Stable identifier (from element.id or auto-generated)
54
+ * @property strategy - If present, this fragment can be reduced during fitting
55
+ * @property index - Position in the fragment list (for stable ordering)
56
+ */
57
+ export interface PromptFragment {
58
+ content: string;
59
+ tokens: number;
60
+ priority: number;
61
+ regionId: string;
62
+ strategy?: Strategy;
63
+ index: number;
64
+ }
65
+ /**
66
+ * A strategy function that reduces a fragment when the prompt is over budget.
67
+ *
68
+ * Strategies are called during the fit loop, starting with the least important
69
+ * priority (highest number). They receive context about the current state and
70
+ * must return replacement fragments (or empty array to remove entirely).
71
+ *
72
+ * Strategies must be:
73
+ * - **Pure**: Don't mutate the input fragments
74
+ * - **Deterministic**: Same input = same output
75
+ * - **Idempotent**: Applying twice has no additional effect
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // A strategy that removes the fragment entirely
80
+ * const omitStrategy: Strategy = () => [];
81
+ *
82
+ * // A strategy that truncates from the end
83
+ * const truncateStrategy: Strategy = ({ target, tokenizer }) => {
84
+ * let content = target.content.slice(0, 100);
85
+ * return [{ ...target, content, tokens: tokenizer(content) }];
86
+ * };
87
+ * ```
88
+ */
89
+ export type Strategy = (input: StrategyInput) => PromptFragment[];
90
+ /**
91
+ * Context passed to strategy functions during the fit loop.
92
+ *
93
+ * @property fragments - All current fragments (readonly, don't mutate)
94
+ * @property target - The specific fragment this strategy should reduce
95
+ * @property budget - The total token budget we're trying to fit within
96
+ * @property tokenizer - Function to count tokens in a string
97
+ * @property totalTokens - Current total token count across all fragments
98
+ * @property iteration - Which iteration of the fit loop (for debugging)
99
+ */
100
+ export interface StrategyInput {
101
+ fragments: readonly PromptFragment[];
102
+ target: PromptFragment;
103
+ budget: number;
104
+ tokenizer: Tokenizer;
105
+ totalTokens: number;
106
+ iteration: number;
107
+ }
108
+ /**
109
+ * A function that counts tokens in a string.
110
+ *
111
+ * Cria doesn't bundle a tokenizer—you provide one. Common choices:
112
+ * - `tiktoken` for OpenAI models (cl100k_base for GPT-4)
113
+ * - Simple approximation: `text => Math.ceil(text.length / 4)`
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * import { encoding_for_model } from "tiktoken";
118
+ *
119
+ * const enc = encoding_for_model("gpt-4");
120
+ * const tokenizer: Tokenizer = (text) => enc.encode(text).length;
121
+ * ```
122
+ */
123
+ export type Tokenizer = (text: string) => number;
124
+ /**
125
+ * Error thrown when the prompt cannot be fit within the budget.
126
+ *
127
+ * This happens when:
128
+ * - No strategies remain but still over budget
129
+ * - Strategies made no progress (possible infinite loop)
130
+ *
131
+ * @property overBudgetBy - How many tokens over budget
132
+ * @property priority - The priority level where fitting failed (-1 if no strategies)
133
+ * @property iteration - Which iteration of the fit loop failed
134
+ */
135
+ export declare class FitError extends Error {
136
+ overBudgetBy: number;
137
+ priority: number;
138
+ iteration: number;
139
+ constructor(overBudgetBy: number, priority: number, iteration: number);
140
+ }
141
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,SAAS,cAAc,EAAE,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;CACtC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,cAAc,EAAE,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;gBAEN,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAStE"}
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Error thrown when the prompt cannot be fit within the budget.
3
+ *
4
+ * This happens when:
5
+ * - No strategies remain but still over budget
6
+ * - Strategies made no progress (possible infinite loop)
7
+ *
8
+ * @property overBudgetBy - How many tokens over budget
9
+ * @property priority - The priority level where fitting failed (-1 if no strategies)
10
+ * @property iteration - Which iteration of the fit loop failed
11
+ */
12
+ export class FitError extends Error {
13
+ overBudgetBy;
14
+ priority;
15
+ iteration;
16
+ constructor(overBudgetBy, priority, iteration) {
17
+ super(`Cannot fit prompt: ${overBudgetBy} tokens over budget at priority ${priority} (iteration ${iteration})`);
18
+ this.name = "FitError";
19
+ this.overBudgetBy = overBudgetBy;
20
+ this.priority = priority;
21
+ this.iteration = iteration;
22
+ }
23
+ }
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAwIA;;;;;;;;;;GAUG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,YAAY,CAAS;IACrB,QAAQ,CAAS;IACjB,SAAS,CAAS;IAElB,YAAY,YAAoB,EAAE,QAAgB,EAAE,SAAiB;QACnE,KAAK,CACH,sBAAsB,YAAY,mCAAmC,QAAQ,eAAe,SAAS,GAAG,CACzG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@fastpaca/cria",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight, fast, and tiny LLM Context & Memory layout renderer to enforce token budgets in long running agents.",
5
+ "license": "MIT",
6
+ "author": "seb@fastpaca.com",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./jsx-runtime": {
17
+ "types": "./dist/jsx-runtime.d.ts",
18
+ "import": "./dist/jsx-runtime.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "check": "ultracite check",
27
+ "fix": "ultracite fix",
28
+ "test": "tsx --test src/**/*.test.tsx",
29
+ "prepare": "husky"
30
+ },
31
+ "devDependencies": {
32
+ "@biomejs/biome": "^2.3.10",
33
+ "@types/node": "^22.0.0",
34
+ "husky": "^9.1.7",
35
+ "tsx": "^4.21.0",
36
+ "typescript": "^5.7.0",
37
+ "ultracite": "^6.5.0"
38
+ }
39
+ }