promptloom 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/README.zh-CN.md +388 -0
- package/bin/cli.ts +275 -0
- package/dist/index.cjs +522 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +611 -0
- package/dist/index.d.ts +611 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/boundary.ts +85 -0
- package/src/compiler.test.ts +625 -0
- package/src/compiler.ts +315 -0
- package/src/index.ts +98 -0
- package/src/providers.ts +160 -0
- package/src/section.ts +103 -0
- package/src/tokens.ts +170 -0
- package/src/tool.ts +128 -0
- package/src/types.ts +214 -0
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promptloom — The Prompt Compiler
|
|
3
|
+
*
|
|
4
|
+
* Implements Claude Code's prompt assembly pattern, generalized:
|
|
5
|
+
*
|
|
6
|
+
* 1. Multi-zone cache scoping (N blocks, not just 2)
|
|
7
|
+
* 2. Conditional section inclusion (when predicates)
|
|
8
|
+
* 3. Static/dynamic section caching
|
|
9
|
+
* 4. Tool schemas with embedded prompts and deferred loading
|
|
10
|
+
* 5. Stable tool ordering for cache hits
|
|
11
|
+
* 6. Token estimation and budget tracking
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
*
|
|
15
|
+
* const pc = new PromptCompiler()
|
|
16
|
+
*
|
|
17
|
+
* // Zone 1: no-cache header
|
|
18
|
+
* pc.zone(null)
|
|
19
|
+
* pc.static('attribution', 'x-model: claude')
|
|
20
|
+
*
|
|
21
|
+
* // Zone 2: globally cacheable
|
|
22
|
+
* pc.zone('global')
|
|
23
|
+
* pc.static('identity', 'You are a coding assistant.')
|
|
24
|
+
* pc.static('rules', () => loadRules())
|
|
25
|
+
*
|
|
26
|
+
* // Zone 3: session-specific
|
|
27
|
+
* pc.zone(null)
|
|
28
|
+
* pc.dynamic('git', async () => gitStatus())
|
|
29
|
+
* pc.static('opus_only', 'Use extended thinking.', {
|
|
30
|
+
* when: (ctx) => ctx.model?.includes('opus'),
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* // Tools
|
|
34
|
+
* pc.tool({ name: 'bash', prompt: '...', inputSchema: {...} })
|
|
35
|
+
* pc.tool({ name: 'rare_tool', prompt: '...', inputSchema: {...}, deferred: true })
|
|
36
|
+
*
|
|
37
|
+
* const result = await pc.compile({ model: 'claude-opus-4-6' })
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import type {
|
|
41
|
+
CacheBlock,
|
|
42
|
+
CacheScope,
|
|
43
|
+
CompileContext,
|
|
44
|
+
CompileResult,
|
|
45
|
+
CompilerOptions,
|
|
46
|
+
ComputeFn,
|
|
47
|
+
Entry,
|
|
48
|
+
Section,
|
|
49
|
+
SectionOptions,
|
|
50
|
+
TokenEstimate,
|
|
51
|
+
ToolDef,
|
|
52
|
+
ZoneMarker,
|
|
53
|
+
} from './types.ts'
|
|
54
|
+
import { SectionCache, resolveSections } from './section.ts'
|
|
55
|
+
import { ToolCache, compileTools } from './tool.ts'
|
|
56
|
+
import { estimateTokens } from './tokens.ts'
|
|
57
|
+
|
|
58
|
+
export class PromptCompiler {
|
|
59
|
+
private entries: Entry[] = []
|
|
60
|
+
private tools: ToolDef[] = []
|
|
61
|
+
private sectionCache: SectionCache
|
|
62
|
+
private toolCache: ToolCache
|
|
63
|
+
private options: Required<CompilerOptions>
|
|
64
|
+
|
|
65
|
+
constructor(options: CompilerOptions = {}) {
|
|
66
|
+
this.options = {
|
|
67
|
+
defaultCacheScope: options.defaultCacheScope ?? 'org',
|
|
68
|
+
dynamicCacheScope: options.dynamicCacheScope ?? null,
|
|
69
|
+
bytesPerToken: options.bytesPerToken ?? 4,
|
|
70
|
+
enableGlobalCache: options.enableGlobalCache ?? false,
|
|
71
|
+
}
|
|
72
|
+
this.sectionCache = new SectionCache()
|
|
73
|
+
this.toolCache = new ToolCache()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Zone API ────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start a new cache zone. All sections after this marker are compiled
|
|
80
|
+
* into a single CacheBlock with the specified scope.
|
|
81
|
+
*
|
|
82
|
+
* Generalizes Claude Code's `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` to support
|
|
83
|
+
* N zones instead of just 2.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* pc.zone(null) // no-cache zone (attribution, headers)
|
|
87
|
+
* pc.zone('global') // globally cacheable (identity, rules)
|
|
88
|
+
* pc.zone('org') // org-level cacheable
|
|
89
|
+
* pc.zone(null) // session-specific (dynamic context)
|
|
90
|
+
*/
|
|
91
|
+
zone(scope: CacheScope): this {
|
|
92
|
+
this.entries.push({ __type: 'zone', scope })
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Insert a cache boundary. Shorthand for starting a new zone with
|
|
98
|
+
* `dynamicCacheScope` (default: null).
|
|
99
|
+
*
|
|
100
|
+
* Equivalent to Claude Code's `SYSTEM_PROMPT_DYNAMIC_BOUNDARY`.
|
|
101
|
+
* Only effective when `enableGlobalCache` is true (otherwise a no-op
|
|
102
|
+
* since there's no scope difference to split on).
|
|
103
|
+
*
|
|
104
|
+
* For explicit multi-zone control, use `zone()` instead.
|
|
105
|
+
*/
|
|
106
|
+
boundary(): this {
|
|
107
|
+
if (this.options.enableGlobalCache) {
|
|
108
|
+
return this.zone(this.options.dynamicCacheScope)
|
|
109
|
+
}
|
|
110
|
+
// When global cache is disabled, boundary is a no-op
|
|
111
|
+
// (all sections use defaultCacheScope anyway)
|
|
112
|
+
return this
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Section API ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add a static section. Computed once and cached for the session.
|
|
119
|
+
*
|
|
120
|
+
* Equivalent to Claude Code's `systemPromptSection()`.
|
|
121
|
+
*
|
|
122
|
+
* @param options.when - Conditional predicate. Section is skipped when false.
|
|
123
|
+
*/
|
|
124
|
+
static(name: string, content: string | ComputeFn, options?: SectionOptions): this {
|
|
125
|
+
const compute = typeof content === 'string' ? () => content : content
|
|
126
|
+
this.entries.push({ name, compute, cacheBreak: false, when: options?.when })
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add a dynamic section. Recomputed every compile() call.
|
|
132
|
+
*
|
|
133
|
+
* Equivalent to Claude Code's `DANGEROUS_uncachedSystemPromptSection()`.
|
|
134
|
+
*
|
|
135
|
+
* @param options.when - Conditional predicate. Section is skipped when false.
|
|
136
|
+
*/
|
|
137
|
+
dynamic(name: string, compute: ComputeFn, options?: SectionOptions): this {
|
|
138
|
+
this.entries.push({ name, compute, cacheBreak: true, when: options?.when })
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Tool API ────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Register a tool with an embedded prompt.
|
|
146
|
+
*
|
|
147
|
+
* Tools with `deferred: true` are compiled separately and excluded
|
|
148
|
+
* from the main tool list. They can be discovered on demand.
|
|
149
|
+
*
|
|
150
|
+
* Tools with `order` fields are sorted for stable serialization
|
|
151
|
+
* (cache hit optimization).
|
|
152
|
+
*/
|
|
153
|
+
tool(def: ToolDef): this {
|
|
154
|
+
this.tools.push(def)
|
|
155
|
+
return this
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Compile ─────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compile the prompt: resolve sections per zone, compile tools,
|
|
162
|
+
* separate deferred tools, and estimate tokens.
|
|
163
|
+
*
|
|
164
|
+
* @param context - Optional context for conditional section evaluation.
|
|
165
|
+
* Sections with `when` predicates are evaluated against this.
|
|
166
|
+
*/
|
|
167
|
+
async compile(context?: CompileContext): Promise<CompileResult> {
|
|
168
|
+
// 1. Group entries into zones
|
|
169
|
+
const zoneGroups = this.groupIntoZones()
|
|
170
|
+
|
|
171
|
+
// 2. Resolve each zone's sections → CacheBlock[]
|
|
172
|
+
const blocks: CacheBlock[] = []
|
|
173
|
+
for (const group of zoneGroups) {
|
|
174
|
+
const resolved = await resolveSections(group.sections, this.sectionCache, context)
|
|
175
|
+
const text = resolved.filter((s): s is string => s !== null).join('\n\n')
|
|
176
|
+
if (text) {
|
|
177
|
+
blocks.push({ text, cacheScope: group.scope })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 3. Separate inline vs deferred tools
|
|
182
|
+
const inlineToolDefs = this.tools.filter((t) => !t.deferred)
|
|
183
|
+
const deferredToolDefs = this.tools.filter((t) => t.deferred)
|
|
184
|
+
|
|
185
|
+
// 4. Compile tools (prompts resolved and cached, sorted by order)
|
|
186
|
+
const compiledTools = await compileTools(inlineToolDefs, this.toolCache)
|
|
187
|
+
const compiledDeferred = await compileTools(deferredToolDefs, this.toolCache)
|
|
188
|
+
|
|
189
|
+
// 5. Estimate tokens
|
|
190
|
+
const bpt = this.options.bytesPerToken
|
|
191
|
+
const systemPromptTokens = blocks.reduce(
|
|
192
|
+
(sum, b) => sum + estimateTokens(b.text, bpt),
|
|
193
|
+
0,
|
|
194
|
+
)
|
|
195
|
+
const toolTokens = compiledTools.reduce(
|
|
196
|
+
(sum, t) => sum + this.estimateToolTokens(t),
|
|
197
|
+
0,
|
|
198
|
+
)
|
|
199
|
+
const deferredToolTokens = compiledDeferred.reduce(
|
|
200
|
+
(sum, t) => sum + this.estimateToolTokens(t),
|
|
201
|
+
0,
|
|
202
|
+
)
|
|
203
|
+
const tokens: TokenEstimate = {
|
|
204
|
+
systemPrompt: systemPromptTokens,
|
|
205
|
+
tools: toolTokens,
|
|
206
|
+
deferredTools: deferredToolTokens,
|
|
207
|
+
total: systemPromptTokens + toolTokens,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 6. Build the plain text version
|
|
211
|
+
const text = blocks.map((b) => b.text).join('\n\n')
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
blocks,
|
|
215
|
+
tools: compiledTools,
|
|
216
|
+
deferredTools: compiledDeferred,
|
|
217
|
+
tokens,
|
|
218
|
+
text,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Cache Management ────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/** Clear all caches. Call on `/clear` or `/compact`. */
|
|
225
|
+
clearCache(): void {
|
|
226
|
+
this.sectionCache.clear()
|
|
227
|
+
this.toolCache.clear()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Clear only section cache (tools stay cached) */
|
|
231
|
+
clearSectionCache(): void {
|
|
232
|
+
this.sectionCache.clear()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Clear only tool cache (forces prompt re-resolution) */
|
|
236
|
+
clearToolCache(): void {
|
|
237
|
+
this.toolCache.clear()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Inspection ──────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/** Get the number of registered sections */
|
|
243
|
+
get sectionCount(): number {
|
|
244
|
+
return this.entries.filter((e): e is Section => !('__type' in e)).length
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Get the number of registered tools (inline + deferred) */
|
|
248
|
+
get toolCount(): number {
|
|
249
|
+
return this.tools.length
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** List registered section names with their types */
|
|
253
|
+
listSections(): Array<{ name: string; type: 'static' | 'dynamic' | 'zone' }> {
|
|
254
|
+
return this.entries.map((e) => {
|
|
255
|
+
if ('__type' in e) {
|
|
256
|
+
return { name: `zone:${e.scope ?? 'none'}`, type: 'zone' as const }
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
name: e.name,
|
|
260
|
+
type: e.cacheBreak ? 'dynamic' as const : 'static' as const,
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** List registered tool names */
|
|
266
|
+
listTools(): string[] {
|
|
267
|
+
return this.tools.map((t) => t.name)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Internal ────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Group entries into zones.
|
|
274
|
+
*
|
|
275
|
+
* Entries before any zone marker belong to the "initial zone" whose
|
|
276
|
+
* scope is determined by `enableGlobalCache` and `defaultCacheScope`.
|
|
277
|
+
*/
|
|
278
|
+
private groupIntoZones(): Array<{ scope: CacheScope; sections: Section[] }> {
|
|
279
|
+
const initialScope = this.options.enableGlobalCache
|
|
280
|
+
? 'global'
|
|
281
|
+
: this.options.defaultCacheScope
|
|
282
|
+
|
|
283
|
+
const zones: Array<{ scope: CacheScope; sections: Section[] }> = []
|
|
284
|
+
let currentZone: { scope: CacheScope; sections: Section[] } = {
|
|
285
|
+
scope: initialScope,
|
|
286
|
+
sections: [],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const entry of this.entries) {
|
|
290
|
+
if ('__type' in entry) {
|
|
291
|
+
// Zone marker: finalize current zone and start a new one
|
|
292
|
+
if (currentZone.sections.length > 0) {
|
|
293
|
+
zones.push(currentZone)
|
|
294
|
+
}
|
|
295
|
+
currentZone = { scope: entry.scope, sections: [] }
|
|
296
|
+
} else {
|
|
297
|
+
currentZone.sections.push(entry)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Don't forget the last zone
|
|
302
|
+
if (currentZone.sections.length > 0) {
|
|
303
|
+
zones.push(currentZone)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return zones
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private estimateToolTokens(tool: { description: string; input_schema: Record<string, unknown> }): number {
|
|
310
|
+
return (
|
|
311
|
+
estimateTokens(tool.description, this.options.bytesPerToken) +
|
|
312
|
+
estimateTokens(JSON.stringify(tool.input_schema), 2) // schemas are dense
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promptloom — Prompt Compiler
|
|
3
|
+
*
|
|
4
|
+
* Weave production-grade LLM prompts with cache boundaries,
|
|
5
|
+
* tool injection, and token budgeting.
|
|
6
|
+
*
|
|
7
|
+
* Reverse-engineered from Claude Code's 7-layer prompt architecture.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { PromptCompiler } from 'promptloom'
|
|
12
|
+
*
|
|
13
|
+
* const pc = new PromptCompiler()
|
|
14
|
+
*
|
|
15
|
+
* pc.zone(null) // no-cache header
|
|
16
|
+
* pc.static('header', 'x-model: claude')
|
|
17
|
+
*
|
|
18
|
+
* pc.zone('global') // globally cacheable
|
|
19
|
+
* pc.static('identity', 'You are a coding assistant.')
|
|
20
|
+
* pc.static('rules', 'Follow clean code principles.')
|
|
21
|
+
*
|
|
22
|
+
* pc.zone(null) // session-specific
|
|
23
|
+
* pc.dynamic('context', async () => `Branch: main`)
|
|
24
|
+
* pc.static('opus_only', 'Use extended thinking.', {
|
|
25
|
+
* when: (ctx) => ctx.model?.includes('opus'),
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* pc.tool({
|
|
29
|
+
* name: 'read_file',
|
|
30
|
+
* prompt: 'Read a file. Always use absolute paths.',
|
|
31
|
+
* inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* pc.tool({
|
|
35
|
+
* name: 'web_search',
|
|
36
|
+
* prompt: 'Search the web.',
|
|
37
|
+
* inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
|
|
38
|
+
* deferred: true, // loaded on demand, not in system prompt
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* const result = await pc.compile({ model: 'claude-opus-4-6' })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
// Core
|
|
46
|
+
export { PromptCompiler } from './compiler.ts'
|
|
47
|
+
|
|
48
|
+
// Section helpers
|
|
49
|
+
export { section, dynamicSection, SectionCache, resolveSections } from './section.ts'
|
|
50
|
+
|
|
51
|
+
// Cache boundary (low-level utility, kept for backward compat)
|
|
52
|
+
export { CACHE_BOUNDARY, splitAtBoundary } from './boundary.ts'
|
|
53
|
+
|
|
54
|
+
// Provider formatters
|
|
55
|
+
export { toAnthropic, toOpenAI, toBedrock, toAnthropicBlocks } from './providers.ts'
|
|
56
|
+
|
|
57
|
+
// Tool helpers
|
|
58
|
+
export { defineTool, ToolCache, compileTool, compileTools } from './tool.ts'
|
|
59
|
+
|
|
60
|
+
// Token utilities
|
|
61
|
+
export {
|
|
62
|
+
estimateTokens,
|
|
63
|
+
estimateTokensForFileType,
|
|
64
|
+
createBudgetTracker,
|
|
65
|
+
checkBudget,
|
|
66
|
+
parseTokenBudget,
|
|
67
|
+
} from './tokens.ts'
|
|
68
|
+
|
|
69
|
+
// Types
|
|
70
|
+
export type {
|
|
71
|
+
Section,
|
|
72
|
+
ComputeFn,
|
|
73
|
+
WhenPredicate,
|
|
74
|
+
CacheScope,
|
|
75
|
+
CacheBlock,
|
|
76
|
+
CompileContext,
|
|
77
|
+
SectionOptions,
|
|
78
|
+
ZoneMarker,
|
|
79
|
+
Entry,
|
|
80
|
+
ToolDef,
|
|
81
|
+
CompiledTool,
|
|
82
|
+
JsonSchema,
|
|
83
|
+
TokenEstimate,
|
|
84
|
+
TokenBudgetConfig,
|
|
85
|
+
CompilerOptions,
|
|
86
|
+
CompileResult,
|
|
87
|
+
ProviderFormat,
|
|
88
|
+
} from './types.ts'
|
|
89
|
+
export type { BudgetTracker, BudgetDecision } from './tokens.ts'
|
|
90
|
+
export type { SplitOptions } from './boundary.ts'
|
|
91
|
+
export type {
|
|
92
|
+
AnthropicCacheControl,
|
|
93
|
+
AnthropicTextBlock,
|
|
94
|
+
AnthropicTool,
|
|
95
|
+
OpenAITool,
|
|
96
|
+
BedrockSystemBlock,
|
|
97
|
+
BedrockTool,
|
|
98
|
+
} from './providers.ts'
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promptloom — Multi-provider output formatting
|
|
3
|
+
*
|
|
4
|
+
* Claude Code supports multiple API providers:
|
|
5
|
+
* - Anthropic (1P): cache_control with scope on text blocks
|
|
6
|
+
* - AWS Bedrock: cache_control without scope
|
|
7
|
+
* - Google Vertex: cache_control without scope
|
|
8
|
+
* - OpenAI: single string system prompt, no caching API
|
|
9
|
+
*
|
|
10
|
+
* This module formats CompileResult for each provider.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CacheBlock, CompiledTool, CompileResult } from './types.ts'
|
|
14
|
+
|
|
15
|
+
// ─── Anthropic ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface AnthropicCacheControl {
|
|
18
|
+
type: 'ephemeral'
|
|
19
|
+
ttl?: '5m' | '1h'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AnthropicTextBlock {
|
|
23
|
+
type: 'text'
|
|
24
|
+
text: string
|
|
25
|
+
cache_control?: AnthropicCacheControl
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AnthropicTool {
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
input_schema: Record<string, unknown>
|
|
32
|
+
cache_control?: AnthropicCacheControl
|
|
33
|
+
defer_loading?: true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format compile result for Anthropic Messages API.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors Claude Code's `buildSystemPromptBlocks()`.
|
|
40
|
+
* Blocks with a non-null cacheScope get `cache_control: { type: 'ephemeral' }`.
|
|
41
|
+
*/
|
|
42
|
+
export function toAnthropic(result: CompileResult): {
|
|
43
|
+
system: AnthropicTextBlock[]
|
|
44
|
+
tools: AnthropicTool[]
|
|
45
|
+
} {
|
|
46
|
+
return {
|
|
47
|
+
system: result.blocks.map((block) => ({
|
|
48
|
+
type: 'text' as const,
|
|
49
|
+
text: block.text,
|
|
50
|
+
...(block.cacheScope !== null
|
|
51
|
+
? { cache_control: { type: 'ephemeral' as const } }
|
|
52
|
+
: {}),
|
|
53
|
+
})),
|
|
54
|
+
tools: [...result.tools, ...result.deferredTools].map((tool) => ({
|
|
55
|
+
name: tool.name,
|
|
56
|
+
description: tool.description,
|
|
57
|
+
input_schema: tool.input_schema,
|
|
58
|
+
...(tool.cache_control ? { cache_control: tool.cache_control } : {}),
|
|
59
|
+
...(tool.defer_loading ? { defer_loading: true as const } : {}),
|
|
60
|
+
})),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── OpenAI ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export interface OpenAITool {
|
|
67
|
+
type: 'function'
|
|
68
|
+
function: {
|
|
69
|
+
name: string
|
|
70
|
+
description: string
|
|
71
|
+
parameters: Record<string, unknown>
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format compile result for OpenAI Chat Completions API.
|
|
77
|
+
*
|
|
78
|
+
* OpenAI uses a single string for system prompt (no block-level caching).
|
|
79
|
+
* Tools are wrapped in the `{ type: 'function', function: {...} }` format.
|
|
80
|
+
*/
|
|
81
|
+
export function toOpenAI(result: CompileResult): {
|
|
82
|
+
system: string
|
|
83
|
+
tools: OpenAITool[]
|
|
84
|
+
} {
|
|
85
|
+
return {
|
|
86
|
+
system: result.text,
|
|
87
|
+
tools: result.tools.map((tool) => ({
|
|
88
|
+
type: 'function' as const,
|
|
89
|
+
function: {
|
|
90
|
+
name: tool.name,
|
|
91
|
+
description: tool.description,
|
|
92
|
+
parameters: tool.input_schema,
|
|
93
|
+
},
|
|
94
|
+
})),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── AWS Bedrock ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export type BedrockSystemBlock =
|
|
101
|
+
| { text: string }
|
|
102
|
+
| { cachePoint: { type: 'default' } }
|
|
103
|
+
|
|
104
|
+
export interface BedrockTool {
|
|
105
|
+
toolSpec: {
|
|
106
|
+
name: string
|
|
107
|
+
description: string
|
|
108
|
+
inputSchema: { jsonSchema: Record<string, unknown> }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format compile result for AWS Bedrock Converse API.
|
|
114
|
+
*
|
|
115
|
+
* Bedrock uses `cachePoint: { type: 'default' }` instead of Anthropic's
|
|
116
|
+
* `cache_control: { type: 'ephemeral' }`. Tools use `toolSpec` wrapper.
|
|
117
|
+
*/
|
|
118
|
+
export function toBedrock(result: CompileResult): {
|
|
119
|
+
system: BedrockSystemBlock[]
|
|
120
|
+
toolConfig: { tools: BedrockTool[] }
|
|
121
|
+
} {
|
|
122
|
+
return {
|
|
123
|
+
system: result.blocks.flatMap((block): BedrockSystemBlock[] => [
|
|
124
|
+
{ text: block.text },
|
|
125
|
+
...(block.cacheScope !== null
|
|
126
|
+
? [{ cachePoint: { type: 'default' as const } }]
|
|
127
|
+
: []),
|
|
128
|
+
]),
|
|
129
|
+
toolConfig: {
|
|
130
|
+
tools: result.tools.map((tool) => ({
|
|
131
|
+
toolSpec: {
|
|
132
|
+
name: tool.name,
|
|
133
|
+
description: tool.description,
|
|
134
|
+
inputSchema: { jsonSchema: tool.input_schema },
|
|
135
|
+
},
|
|
136
|
+
})),
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Convenience: toAnthropicBlocks (backward compat) ────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Convert cache blocks to Anthropic API text block format.
|
|
145
|
+
*
|
|
146
|
+
* @deprecated Use `toAnthropic(result).system` instead for full formatting.
|
|
147
|
+
* Kept for backward compatibility.
|
|
148
|
+
*/
|
|
149
|
+
export function toAnthropicBlocks(
|
|
150
|
+
blocks: CacheBlock[],
|
|
151
|
+
enableCaching = true,
|
|
152
|
+
): AnthropicTextBlock[] {
|
|
153
|
+
return blocks.map((block) => ({
|
|
154
|
+
type: 'text' as const,
|
|
155
|
+
text: block.text,
|
|
156
|
+
...(enableCaching && block.cacheScope !== null
|
|
157
|
+
? { cache_control: { type: 'ephemeral' as const } }
|
|
158
|
+
: {}),
|
|
159
|
+
}))
|
|
160
|
+
}
|
package/src/section.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promptloom — Section management
|
|
3
|
+
*
|
|
4
|
+
* Implements the two-tier section system from Claude Code:
|
|
5
|
+
* - `section()`: cached within session, computed once
|
|
6
|
+
* - `dynamicSection()`: recomputed every compile() call (cacheBreak: true)
|
|
7
|
+
*
|
|
8
|
+
* Extended with conditional inclusion via `when` predicates,
|
|
9
|
+
* mirroring Claude Code's `feature()` and `process.env.USER_TYPE` gates.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CompileContext, ComputeFn, Section, SectionOptions, WhenPredicate } from './types.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a static section. Content is computed once and cached.
|
|
16
|
+
*
|
|
17
|
+
* Use for: identity prompts, rules, style guides — anything that
|
|
18
|
+
* doesn't change between turns.
|
|
19
|
+
*/
|
|
20
|
+
export function section(name: string, compute: ComputeFn, options?: SectionOptions): Section {
|
|
21
|
+
return { name, compute, cacheBreak: false, when: options?.when }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a dynamic section. Content is recomputed every compile() call.
|
|
26
|
+
*
|
|
27
|
+
* Claude Code calls this `DANGEROUS_uncachedSystemPromptSection` —
|
|
28
|
+
* the naming reflects that dynamic sections break prompt cache stability.
|
|
29
|
+
*
|
|
30
|
+
* Use for: MCP server instructions, real-time status, anything that
|
|
31
|
+
* changes between turns.
|
|
32
|
+
*/
|
|
33
|
+
export function dynamicSection(name: string, compute: ComputeFn, options?: SectionOptions): Section {
|
|
34
|
+
return { name, compute, cacheBreak: true, when: options?.when }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Section Cache ───────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-memory section cache.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors `STATE.systemPromptSectionCache` from Claude Code.
|
|
43
|
+
* Static sections are computed once per session and cached here.
|
|
44
|
+
* Dynamic sections (cacheBreak: true) bypass the cache entirely.
|
|
45
|
+
*/
|
|
46
|
+
export class SectionCache {
|
|
47
|
+
private cache = new Map<string, string | null>()
|
|
48
|
+
|
|
49
|
+
get(name: string): string | null | undefined {
|
|
50
|
+
return this.cache.get(name)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
has(name: string): boolean {
|
|
54
|
+
return this.cache.has(name)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
set(name: string, value: string | null): void {
|
|
58
|
+
this.cache.set(name, value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear(): void {
|
|
62
|
+
this.cache.clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get size(): number {
|
|
66
|
+
return this.cache.size
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve an array of sections, using cache for static ones.
|
|
72
|
+
*
|
|
73
|
+
* Mirrors `resolveSystemPromptSections()` from Claude Code:
|
|
74
|
+
* - Sections with a `when` predicate are filtered by compile context
|
|
75
|
+
* - Static sections: check cache first, compute if missing
|
|
76
|
+
* - Dynamic sections: always recompute
|
|
77
|
+
* - All sections resolved in parallel via Promise.all
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveSections(
|
|
80
|
+
sections: Section[],
|
|
81
|
+
cache: SectionCache,
|
|
82
|
+
context?: CompileContext,
|
|
83
|
+
): Promise<(string | null)[]> {
|
|
84
|
+
// Filter by when predicate
|
|
85
|
+
const ctx = context ?? {}
|
|
86
|
+
const active = sections.filter((s) => !s.when || s.when(ctx))
|
|
87
|
+
|
|
88
|
+
return Promise.all(
|
|
89
|
+
active.map(async (s) => {
|
|
90
|
+
// Dynamic sections always recompute
|
|
91
|
+
if (!s.cacheBreak && cache.has(s.name)) {
|
|
92
|
+
return cache.get(s.name) ?? null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const value = await s.compute()
|
|
96
|
+
// Only cache static sections
|
|
97
|
+
if (!s.cacheBreak) {
|
|
98
|
+
cache.set(s.name, value)
|
|
99
|
+
}
|
|
100
|
+
return value
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
}
|