@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 +21 -0
- package/README.md +165 -0
- package/dist/components.d.ts +78 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +98 -0
- package/dist/components.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-runtime.d.ts +18 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +40 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/render.d.ts +44 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +171 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.d.ts +2 -0
- package/dist/render.test.d.ts.map +1 -0
- package/dist/render.test.js +49 -0
- package/dist/render.test.js.map +1 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
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"}
|
package/dist/index.d.ts
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
|
+
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"}
|
package/dist/render.d.ts
ADDED
|
@@ -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 @@
|
|
|
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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|