@falai/agent 1.1.2 → 1.2.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.
Files changed (173) hide show
  1. package/README.md +9 -0
  2. package/dist/cjs/core/Agent.d.ts +17 -1
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +47 -0
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/BatchPromptBuilder.d.ts +3 -0
  7. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
  8. package/dist/cjs/core/BatchPromptBuilder.js +14 -11
  9. package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
  10. package/dist/cjs/core/CompactionEngine.d.ts +65 -0
  11. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -0
  12. package/dist/cjs/core/CompactionEngine.js +251 -0
  13. package/dist/cjs/core/CompactionEngine.js.map +1 -0
  14. package/dist/cjs/core/PromptComposer.d.ts +8 -1
  15. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  16. package/dist/cjs/core/PromptComposer.js +238 -118
  17. package/dist/cjs/core/PromptComposer.js.map +1 -1
  18. package/dist/cjs/core/PromptSectionCache.d.ts +57 -0
  19. package/dist/cjs/core/PromptSectionCache.d.ts.map +1 -0
  20. package/dist/cjs/core/PromptSectionCache.js +108 -0
  21. package/dist/cjs/core/PromptSectionCache.js.map +1 -0
  22. package/dist/cjs/core/ResponseEngine.d.ts +3 -0
  23. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  24. package/dist/cjs/core/ResponseEngine.js +10 -6
  25. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  26. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  27. package/dist/cjs/core/ResponseModal.js +79 -20
  28. package/dist/cjs/core/ResponseModal.js.map +1 -1
  29. package/dist/cjs/core/RoutingEngine.d.ts +10 -0
  30. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  31. package/dist/cjs/core/RoutingEngine.js +3 -2
  32. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  33. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  34. package/dist/cjs/core/SessionManager.js +20 -0
  35. package/dist/cjs/core/SessionManager.js.map +1 -1
  36. package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
  37. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
  38. package/dist/cjs/core/StreamingToolExecutor.js +455 -0
  39. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
  40. package/dist/cjs/core/ToolManager.d.ts +18 -1
  41. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  42. package/dist/cjs/core/ToolManager.js +91 -0
  43. package/dist/cjs/core/ToolManager.js.map +1 -1
  44. package/dist/cjs/index.d.ts +5 -1
  45. package/dist/cjs/index.d.ts.map +1 -1
  46. package/dist/cjs/index.js +8 -2
  47. package/dist/cjs/index.js.map +1 -1
  48. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  49. package/dist/cjs/providers/AnthropicProvider.js +8 -7
  50. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  51. package/dist/cjs/providers/GeminiProvider.d.ts +25 -0
  52. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  53. package/dist/cjs/providers/GeminiProvider.js +79 -51
  54. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  55. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  56. package/dist/cjs/providers/OpenAIProvider.js +14 -6
  57. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  58. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  59. package/dist/cjs/providers/OpenRouterProvider.js +7 -6
  60. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  61. package/dist/cjs/types/agent.d.ts +44 -0
  62. package/dist/cjs/types/agent.d.ts.map +1 -1
  63. package/dist/cjs/types/agent.js.map +1 -1
  64. package/dist/cjs/types/compaction.d.ts +50 -0
  65. package/dist/cjs/types/compaction.d.ts.map +1 -0
  66. package/dist/cjs/types/compaction.js +6 -0
  67. package/dist/cjs/types/compaction.js.map +1 -0
  68. package/dist/cjs/types/index.d.ts +4 -2
  69. package/dist/cjs/types/index.d.ts.map +1 -1
  70. package/dist/cjs/types/index.js.map +1 -1
  71. package/dist/cjs/types/tool.d.ts +84 -0
  72. package/dist/cjs/types/tool.d.ts.map +1 -1
  73. package/dist/core/Agent.d.ts +17 -1
  74. package/dist/core/Agent.d.ts.map +1 -1
  75. package/dist/core/Agent.js +47 -0
  76. package/dist/core/Agent.js.map +1 -1
  77. package/dist/core/BatchPromptBuilder.d.ts +3 -0
  78. package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
  79. package/dist/core/BatchPromptBuilder.js +14 -11
  80. package/dist/core/BatchPromptBuilder.js.map +1 -1
  81. package/dist/core/CompactionEngine.d.ts +65 -0
  82. package/dist/core/CompactionEngine.d.ts.map +1 -0
  83. package/dist/core/CompactionEngine.js +244 -0
  84. package/dist/core/CompactionEngine.js.map +1 -0
  85. package/dist/core/PromptComposer.d.ts +8 -1
  86. package/dist/core/PromptComposer.d.ts.map +1 -1
  87. package/dist/core/PromptComposer.js +238 -118
  88. package/dist/core/PromptComposer.js.map +1 -1
  89. package/dist/core/PromptSectionCache.d.ts +57 -0
  90. package/dist/core/PromptSectionCache.d.ts.map +1 -0
  91. package/dist/core/PromptSectionCache.js +104 -0
  92. package/dist/core/PromptSectionCache.js.map +1 -0
  93. package/dist/core/ResponseEngine.d.ts +3 -0
  94. package/dist/core/ResponseEngine.d.ts.map +1 -1
  95. package/dist/core/ResponseEngine.js +10 -6
  96. package/dist/core/ResponseEngine.js.map +1 -1
  97. package/dist/core/ResponseModal.d.ts.map +1 -1
  98. package/dist/core/ResponseModal.js +79 -20
  99. package/dist/core/ResponseModal.js.map +1 -1
  100. package/dist/core/RoutingEngine.d.ts +10 -0
  101. package/dist/core/RoutingEngine.d.ts.map +1 -1
  102. package/dist/core/RoutingEngine.js +3 -2
  103. package/dist/core/RoutingEngine.js.map +1 -1
  104. package/dist/core/SessionManager.d.ts.map +1 -1
  105. package/dist/core/SessionManager.js +17 -0
  106. package/dist/core/SessionManager.js.map +1 -1
  107. package/dist/core/StreamingToolExecutor.d.ts +142 -0
  108. package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
  109. package/dist/core/StreamingToolExecutor.js +448 -0
  110. package/dist/core/StreamingToolExecutor.js.map +1 -0
  111. package/dist/core/ToolManager.d.ts +18 -1
  112. package/dist/core/ToolManager.d.ts.map +1 -1
  113. package/dist/core/ToolManager.js +91 -0
  114. package/dist/core/ToolManager.js.map +1 -1
  115. package/dist/index.d.ts +5 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +3 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  120. package/dist/providers/AnthropicProvider.js +8 -7
  121. package/dist/providers/AnthropicProvider.js.map +1 -1
  122. package/dist/providers/GeminiProvider.d.ts +25 -0
  123. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  124. package/dist/providers/GeminiProvider.js +79 -51
  125. package/dist/providers/GeminiProvider.js.map +1 -1
  126. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  127. package/dist/providers/OpenAIProvider.js +14 -6
  128. package/dist/providers/OpenAIProvider.js.map +1 -1
  129. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  130. package/dist/providers/OpenRouterProvider.js +7 -6
  131. package/dist/providers/OpenRouterProvider.js.map +1 -1
  132. package/dist/types/agent.d.ts +44 -0
  133. package/dist/types/agent.d.ts.map +1 -1
  134. package/dist/types/agent.js.map +1 -1
  135. package/dist/types/compaction.d.ts +50 -0
  136. package/dist/types/compaction.d.ts.map +1 -0
  137. package/dist/types/compaction.js +5 -0
  138. package/dist/types/compaction.js.map +1 -0
  139. package/dist/types/index.d.ts +4 -2
  140. package/dist/types/index.d.ts.map +1 -1
  141. package/dist/types/index.js.map +1 -1
  142. package/dist/types/tool.d.ts +84 -0
  143. package/dist/types/tool.d.ts.map +1 -1
  144. package/docs/api/overview.md +140 -0
  145. package/docs/core/tools/enhanced-tool.md +186 -0
  146. package/docs/core/tools/streaming-execution.md +161 -0
  147. package/docs/guides/context-compaction.md +96 -0
  148. package/docs/guides/prompt-optimization.md +164 -0
  149. package/examples/advanced-patterns/context-compaction.ts +223 -0
  150. package/examples/advanced-patterns/streaming-responses.ts +85 -7
  151. package/examples/tools/enhanced-tool-metadata.ts +268 -0
  152. package/examples/tools/streaming-tool-execution.ts +283 -0
  153. package/package.json +1 -1
  154. package/src/core/Agent.ts +58 -2
  155. package/src/core/BatchPromptBuilder.ts +14 -11
  156. package/src/core/CompactionEngine.ts +318 -0
  157. package/src/core/PromptComposer.ts +261 -141
  158. package/src/core/PromptSectionCache.ts +136 -0
  159. package/src/core/ResponseEngine.ts +9 -6
  160. package/src/core/ResponseModal.ts +81 -20
  161. package/src/core/RoutingEngine.ts +13 -2
  162. package/src/core/SessionManager.ts +19 -0
  163. package/src/core/StreamingToolExecutor.ts +572 -0
  164. package/src/core/ToolManager.ts +151 -41
  165. package/src/index.ts +14 -0
  166. package/src/providers/AnthropicProvider.ts +11 -12
  167. package/src/providers/GeminiProvider.ts +83 -52
  168. package/src/providers/OpenAIProvider.ts +21 -13
  169. package/src/providers/OpenRouterProvider.ts +13 -13
  170. package/src/types/agent.ts +45 -0
  171. package/src/types/compaction.ts +52 -0
  172. package/src/types/index.ts +35 -14
  173. package/src/types/tool.ts +108 -0
@@ -3,41 +3,74 @@ import type { Route } from "./Route";
3
3
  import { render, renderMany, formatKnowledgeBase, createTemplateContext } from "../utils/template";
4
4
  import { TemplateContext } from "../types/template";
5
5
  import { extractAIContextStrings, ConditionEvaluator } from "../utils/condition";
6
+ import { PromptSectionCache } from "./PromptSectionCache";
6
7
 
7
8
  export class PromptComposer<TContext = unknown, TData = unknown> {
8
9
  private parts: string[] = [];
9
10
  private renderContext: TemplateContext<TContext, TData>;
11
+ private cache: PromptSectionCache | null;
12
+ private instructionCounter = 0;
10
13
 
11
- constructor(context: TemplateContext<TContext, TData> = createTemplateContext({})) {
14
+ constructor(
15
+ context: TemplateContext<TContext, TData> = createTemplateContext({}),
16
+ cache?: PromptSectionCache
17
+ ) {
12
18
  this.renderContext = context;
19
+ this.cache = cache ?? null;
13
20
  }
14
21
 
15
22
  // Specific, typed sections tailored to the framework
16
23
 
17
24
  async addAgentMeta(agent: AgentOptions<TContext, TData>): Promise<this> {
18
- const lines: string[] = [];
19
- lines.push("## Agent");
20
- lines.push(`**Name:** ${agent.name}`);
21
- if (agent.goal) {
22
- lines.push(`**Goal:** ${agent.goal}`);
23
- }
24
- if (agent.description) {
25
- lines.push(`**Description:** ${agent.description}`);
26
- }
27
- if (agent.identity) {
25
+ const compute = async (): Promise<string | null> => {
26
+ const lines: string[] = [];
27
+ lines.push("## Agent Identity");
28
28
  lines.push(
29
- `**Identity:** ${await render(agent.identity, this.renderContext)}`
29
+ `You are "${agent.name}". Always refer to yourself by this name.`
30
30
  );
31
- }
32
- if (agent.personality) {
33
- lines.push(
34
- `**Personality:** ${await render(
35
- agent.personality,
31
+ if (agent.identity) {
32
+ lines.push(await render(agent.identity, this.renderContext));
33
+ }
34
+ if (agent.personality) {
35
+ lines.push(
36
+ `Communicate in the following style: ${await render(
37
+ agent.personality,
38
+ this.renderContext
39
+ )}`
40
+ );
41
+ }
42
+ if (agent.goal) {
43
+ lines.push(`Your primary goal: ${agent.goal}`);
44
+ }
45
+ if (agent.description) {
46
+ lines.push(`About you: ${agent.description}`);
47
+ }
48
+ if (agent.rules?.length) {
49
+ const renderedRules = await renderMany(agent.rules, this.renderContext);
50
+ lines.push(
51
+ `You MUST always follow these rules:\n- ${renderedRules.join("\n- ")}`
52
+ );
53
+ }
54
+ if (agent.prohibitions?.length) {
55
+ const renderedProhibitions = await renderMany(
56
+ agent.prohibitions,
36
57
  this.renderContext
37
- )}`
38
- );
58
+ );
59
+ lines.push(
60
+ `You MUST NEVER do the following:\n- ${renderedProhibitions.join(
61
+ "\n- "
62
+ )}`
63
+ );
64
+ }
65
+ return lines.join("\n");
66
+ };
67
+
68
+ if (this.cache) {
69
+ this.cache.register("agentMeta", "static", compute);
70
+ } else {
71
+ const result = await compute();
72
+ if (result) this.parts.push(result);
39
73
  }
40
- this.parts.push(lines.join("\n"));
41
74
  return this;
42
75
  }
43
76
 
@@ -46,58 +79,99 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
46
79
  }
47
80
 
48
81
  async addScoringRules(): Promise<this> {
49
- this.parts.push(
50
- `## Scoring Rules\n\n${[
82
+ const compute = (): string | null => {
83
+ return `## Scoring Rules\n\n${[
51
84
  "- 90-100: explicit keywords + clear intent",
52
85
  "- 70-89: strong contextual evidence + relevant keywords",
53
86
  "- 50-69: moderate relevance",
54
87
  "- 30-49: weak connection or ambiguous",
55
88
  "- 0-29: minimal/none",
56
89
  "Return ONLY JSON matching the provided schema. Include scores for ALL routes.",
57
- ].join("\n")}`
58
- );
90
+ ].join("\n")}`;
91
+ };
92
+
93
+ if (this.cache) {
94
+ this.cache.register("scoringRules", "static", compute);
95
+ } else {
96
+ this.parts.push(compute()!);
97
+ }
59
98
  return Promise.resolve(this);
60
99
  }
61
100
 
62
101
  async addInstruction(text: string): Promise<this> {
63
- if (text) this.parts.push(`## Instruction\n\n${text}`);
102
+ if (!text) return Promise.resolve(this);
103
+
104
+ const content = `## Instruction\n\n${text}`;
105
+
106
+ if (this.cache) {
107
+ const key = `instruction-${this.instructionCounter++}`;
108
+ this.cache.register(key, "dynamic", () => content);
109
+ } else {
110
+ this.parts.push(content);
111
+ }
64
112
  return Promise.resolve(this);
65
113
  }
66
114
 
115
+ /**
116
+ * @deprecated History should flow through `GenerateMessageInput.history` natively.
117
+ * This method is kept for backward compatibility but will be removed in a future version.
118
+ */
67
119
  async addInteractionHistory(history: Event[], note?: string): Promise<this> {
68
- const recent = history
69
- .slice(-10)
70
- .map((e) => `- ${JSON.stringify(e)}`)
71
- .join("\n");
72
- const header = note ? `${note}\n\n` : "";
73
- this.parts.push(
74
- `## Interaction History\n\n${header}Recent conversation events:\n\n${recent}`
75
- );
120
+ const compute = (): string | null => {
121
+ const recent = history
122
+ .slice(-10)
123
+ .map((e) => `- ${JSON.stringify(e)}`)
124
+ .join("\n");
125
+ const header = note ? `${note}\n\n` : "";
126
+ return `## Interaction History\n\n${header}Recent conversation events:\n\n${recent}`;
127
+ };
128
+
129
+ if (this.cache) {
130
+ this.cache.register("interactionHistory", "dynamic", compute);
131
+ } else {
132
+ this.parts.push(compute()!);
133
+ }
76
134
  return Promise.resolve(this);
77
135
  }
78
136
 
79
137
  async addLastMessage(message: string): Promise<this> {
80
- this.parts.push(`## Last Message\n\n${message}`);
138
+ const compute = (): string | null => {
139
+ return `## Last Message\n\n${message}`;
140
+ };
141
+
142
+ if (this.cache) {
143
+ this.cache.register("lastMessage", "dynamic", compute);
144
+ } else {
145
+ this.parts.push(compute()!);
146
+ }
81
147
  return Promise.resolve(this);
82
148
  }
83
149
 
84
150
  async addGlossary(terms: Term<TContext>[]): Promise<this> {
85
151
  if (!terms.length) return this;
86
152
 
87
- const renderedTerms = await Promise.all(
88
- terms.map(async (t) => {
89
- const name = await render(t.name, this.renderContext);
90
- const description = await render(t.description, this.renderContext);
91
- const synonyms = t.synonyms
92
- ? await renderMany(t.synonyms, this.renderContext)
93
- : [];
94
- const synonymText =
95
- synonyms.length > 0 ? ` (synonyms: ${synonyms.join(", ")})` : "";
96
- return `- **${name}**${synonymText}: ${description}`;
97
- })
98
- );
99
-
100
- this.parts.push(`## Glossary\n\n${renderedTerms.join("\n")}`);
153
+ const compute = async (): Promise<string | null> => {
154
+ const renderedTerms = await Promise.all(
155
+ terms.map(async (t) => {
156
+ const name = await render(t.name, this.renderContext);
157
+ const description = await render(t.description, this.renderContext);
158
+ const synonyms = t.synonyms
159
+ ? await renderMany(t.synonyms, this.renderContext)
160
+ : [];
161
+ const synonymText =
162
+ synonyms.length > 0 ? ` (synonyms: ${synonyms.join(", ")})` : "";
163
+ return `- **${name}**${synonymText}: ${description}`;
164
+ })
165
+ );
166
+ return `## Glossary\n\n${renderedTerms.join("\n")}`;
167
+ };
168
+
169
+ if (this.cache) {
170
+ this.cache.register("glossary", "static", compute);
171
+ } else {
172
+ const result = await compute();
173
+ if (result) this.parts.push(result);
174
+ }
101
175
  return this;
102
176
  }
103
177
 
@@ -105,56 +179,64 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
105
179
  const enabled = guidelines.filter((g) => g.enabled !== false);
106
180
  if (!enabled.length) return this;
107
181
 
108
- const evaluator = new ConditionEvaluator(this.renderContext);
109
- const activeGuidelines: Guideline<TContext, TData>[] = [];
110
- const allAIContextStrings: string[] = [];
111
-
112
- // Evaluate guideline conditions to determine which are active
113
- for (const guideline of enabled) {
114
- if (guideline.condition) {
115
- const evaluation = await evaluator.evaluateCondition(guideline.condition, 'AND');
116
-
117
- // Collect AI context strings for prompt
118
- allAIContextStrings.push(...evaluation.aiContextStrings);
119
-
120
- // Include guideline if:
121
- // 1. No programmatic conditions (only strings) - always active
122
- // 2. Programmatic conditions evaluate to true
123
- if (!evaluation.hasProgrammaticConditions || evaluation.programmaticResult) {
182
+ const compute = async (): Promise<string | null> => {
183
+ const evaluator = new ConditionEvaluator(this.renderContext);
184
+ const activeGuidelines: Guideline<TContext, TData>[] = [];
185
+ const allAIContextStrings: string[] = [];
186
+
187
+ // Evaluate guideline conditions to determine which are active
188
+ for (const guideline of enabled) {
189
+ if (guideline.condition) {
190
+ const evaluation = await evaluator.evaluateCondition(guideline.condition, 'AND');
191
+
192
+ // Collect AI context strings for prompt
193
+ allAIContextStrings.push(...evaluation.aiContextStrings);
194
+
195
+ // Include guideline if:
196
+ // 1. No programmatic conditions (only strings) - always active
197
+ // 2. Programmatic conditions evaluate to true
198
+ if (!evaluation.hasProgrammaticConditions || evaluation.programmaticResult) {
199
+ activeGuidelines.push(guideline);
200
+ }
201
+ } else {
202
+ // No condition means always active
124
203
  activeGuidelines.push(guideline);
125
204
  }
126
- } else {
127
- // No condition means always active
128
- activeGuidelines.push(guideline);
129
205
  }
130
- }
131
206
 
132
- if (!activeGuidelines.length && !allAIContextStrings.length) return this;
133
-
134
- const renderedGuidelines = await Promise.all(
135
- activeGuidelines.map(async (g, i) => {
136
- const action = await render(g.action, this.renderContext);
137
- if (g.condition) {
138
- // Use AI context strings if available, otherwise render the condition
139
- const conditionStrings = extractAIContextStrings(g.condition);
140
- if (conditionStrings.length > 0) {
141
- const conditionText = conditionStrings.join(" AND ");
142
- return `- Guideline #${i + 1}: When ${conditionText}, then ${action}`;
207
+ if (!activeGuidelines.length && !allAIContextStrings.length) return null;
208
+
209
+ const renderedGuidelines = await Promise.all(
210
+ activeGuidelines.map(async (g, i) => {
211
+ const action = await render(g.action, this.renderContext);
212
+ if (g.condition) {
213
+ // Use AI context strings if available, otherwise render the condition
214
+ const conditionStrings = extractAIContextStrings(g.condition);
215
+ if (conditionStrings.length > 0) {
216
+ const conditionText = conditionStrings.join(" AND ");
217
+ return `- Guideline #${i + 1}: When ${conditionText}, then ${action}`;
218
+ }
143
219
  }
144
- }
145
- return `- Guideline #${i + 1}: ${action}`;
146
- })
147
- );
148
-
149
- // Add any additional AI context from inactive guidelines
150
- if (allAIContextStrings.length > 0) {
151
- const uniqueContextStrings = Array.from(new Set(allAIContextStrings));
152
- const contextSection = `\n\n**Additional Context:** ${uniqueContextStrings.join(", ")}`;
153
- this.parts.push(`## Guidelines\n\n${renderedGuidelines.join("\n")}${contextSection}`);
220
+ return `- Guideline #${i + 1}: ${action}`;
221
+ })
222
+ );
223
+
224
+ // Add any additional AI context from inactive guidelines
225
+ if (allAIContextStrings.length > 0) {
226
+ const uniqueContextStrings = Array.from(new Set(allAIContextStrings));
227
+ const contextSection = `\n\n**Additional Context:** ${uniqueContextStrings.join(", ")}`;
228
+ return `## Guidelines\n\n${renderedGuidelines.join("\n")}${contextSection}`;
229
+ } else {
230
+ return `## Guidelines\n\n${renderedGuidelines.join("\n")}`;
231
+ }
232
+ };
233
+
234
+ if (this.cache) {
235
+ this.cache.register("guidelines", "dynamic", compute);
154
236
  } else {
155
- this.parts.push(`## Guidelines\n\n${renderedGuidelines.join("\n")}`);
237
+ const result = await compute();
238
+ if (result) this.parts.push(result);
156
239
  }
157
-
158
240
  return this;
159
241
  }
160
242
 
@@ -162,65 +244,88 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
162
244
  agentKnowledgeBase?: Record<string, unknown>,
163
245
  routeKnowledgeBase?: Record<string, unknown>
164
246
  ): Promise<this> {
165
- // Merge agent and route knowledge bases (route takes precedence for conflicts)
166
- const mergedKnowledge = {
167
- ...(agentKnowledgeBase || {}),
168
- ...(routeKnowledgeBase || {}),
247
+ const compute = (): string | null => {
248
+ // Merge agent and route knowledge bases (route takes precedence for conflicts)
249
+ const mergedKnowledge = {
250
+ ...(agentKnowledgeBase || {}),
251
+ ...(routeKnowledgeBase || {}),
252
+ };
253
+
254
+ // Only add section if there's knowledge data
255
+ if (Object.keys(mergedKnowledge).length > 0) {
256
+ return formatKnowledgeBase(mergedKnowledge, "Knowledge Base");
257
+ }
258
+ return null;
169
259
  };
170
260
 
171
- // Only add section if there's knowledge data
172
- if (Object.keys(mergedKnowledge).length > 0) {
173
- const formatted = formatKnowledgeBase(mergedKnowledge, "Knowledge Base");
174
- this.parts.push(formatted);
261
+ if (this.cache) {
262
+ this.cache.register("knowledgeBase", "static", compute);
263
+ } else {
264
+ const result = compute();
265
+ if (result) this.parts.push(result);
175
266
  }
176
-
177
267
  return Promise.resolve(this);
178
268
  }
179
269
 
180
270
  async addActiveRoutes(routes: Route<TContext, TData>[]): Promise<this> {
181
271
  if (!routes.length) return this;
182
272
 
183
- const renderedRoutes = await Promise.all(
184
- routes.map(async (r, i) => {
185
- const whenContextStrings = r.when ? extractAIContextStrings(r.when) : [];
186
- const conditions =
187
- whenContextStrings.length > 0
188
- ? `\n\n **Triggered when:** ${whenContextStrings.join(" OR ")}`
273
+ const compute = async (): Promise<string | null> => {
274
+ const renderedRoutes = await Promise.all(
275
+ routes.map(async (r, i) => {
276
+ const whenContextStrings = r.when ? extractAIContextStrings(r.when) : [];
277
+ const conditions =
278
+ whenContextStrings.length > 0
279
+ ? `\n\n **Triggered when:** ${whenContextStrings.join(" OR ")}`
280
+ : "";
281
+ const desc = r.description
282
+ ? `\n\n **Description:** ${r.description}`
189
283
  : "";
190
- const desc = r.description
191
- ? `\n\n **Description:** ${r.description}`
192
- : "";
193
- const rules = await renderMany(r.getRules(), this.renderContext);
194
- const prohibitions = await renderMany(
195
- r.getProhibitions(),
196
- this.renderContext
197
- );
198
- const rulesInfo =
199
- rules.length > 0
200
- ? `\n\n **Rules:**\n ${rules.map((x) => ` - ${x}`).join("\n ")}`
201
- : "";
202
- const prohibitionsInfo =
203
- prohibitions.length > 0
204
- ? `\n\n **Prohibitions:**\n ${prohibitions
205
- .map((x) => ` - ${x}`)
206
- .join("\n ")}`
207
- : "";
208
- return `### Route ${i + 1}: ${r.title
209
- }${desc}${conditions}${rulesInfo}${prohibitionsInfo}`;
210
- })
211
- );
284
+ const rules = await renderMany(r.getRules(), this.renderContext);
285
+ const prohibitions = await renderMany(
286
+ r.getProhibitions(),
287
+ this.renderContext
288
+ );
289
+ const rulesInfo =
290
+ rules.length > 0
291
+ ? `\n\n **Rules:**\n ${rules.map((x) => ` - ${x}`).join("\n ")}`
292
+ : "";
293
+ const prohibitionsInfo =
294
+ prohibitions.length > 0
295
+ ? `\n\n **Prohibitions:**\n ${prohibitions
296
+ .map((x) => ` - ${x}`)
297
+ .join("\n ")}`
298
+ : "";
299
+ return `### Route ${i + 1}: ${r.title
300
+ }${desc}${conditions}${rulesInfo}${prohibitionsInfo}`;
301
+ })
302
+ );
303
+ return `## Available Routes\n\n${renderedRoutes.join("\n\n")}`;
304
+ };
212
305
 
213
- this.parts.push(`## Available Routes\n\n${renderedRoutes.join("\n\n")}`);
306
+ if (this.cache) {
307
+ this.cache.register("activeRoutes", "static", compute);
308
+ } else {
309
+ const result = await compute();
310
+ if (result) this.parts.push(result);
311
+ }
214
312
  return this;
215
313
  }
216
314
 
217
315
  async addDirectives(directives?: string[]): Promise<this> {
218
316
  if (!directives?.length) return this;
219
- this.parts.push(
220
- `## Directives\n\nAddress concisely:\n\n${directives
317
+
318
+ const compute = (): string | null => {
319
+ return `## Directives\n\nAddress concisely:\n\n${directives
221
320
  .map((d) => `- ${d}`)
222
- .join("\n")}`
223
- );
321
+ .join("\n")}`;
322
+ };
323
+
324
+ if (this.cache) {
325
+ this.cache.register("directives", "dynamic", compute);
326
+ } else {
327
+ this.parts.push(compute()!);
328
+ }
224
329
  return Promise.resolve(this);
225
330
  }
226
331
 
@@ -234,19 +339,34 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
234
339
  ): Promise<this> {
235
340
  if (!tools?.length) return this;
236
341
 
237
- const renderedTools = tools.map((tool, i) => {
238
- const toolName = tool.name || tool.id;
239
- const desc = tool.description
240
- ? `\n Description: ${tool.description}`
241
- : "";
242
- return `### Tool ${i + 1}: ${toolName}${desc}`;
243
- });
342
+ const compute = (): string | null => {
343
+ const renderedTools = tools.map((tool, i) => {
344
+ const toolName = tool.name || tool.id;
345
+ const desc = tool.description
346
+ ? `\n Description: ${tool.description}`
347
+ : "";
348
+ return `### Tool ${i + 1}: ${toolName}${desc}`;
349
+ });
350
+ return `## Available Tools\n\n${renderedTools.join("\n\n")}`;
351
+ };
244
352
 
245
- this.parts.push(`## Available Tools\n\n${renderedTools.join("\n\n")}`);
353
+ if (this.cache) {
354
+ this.cache.register("availableTools", "dynamic", compute);
355
+ } else {
356
+ this.parts.push(compute()!);
357
+ }
246
358
  return Promise.resolve(this);
247
359
  }
248
360
 
249
361
  async build(): Promise<string> {
362
+ if (this.cache) {
363
+ const sections = await this.cache.resolveAll();
364
+ const prompt = sections
365
+ .filter((s): s is string => s != null && s !== "")
366
+ .join("\n\n")
367
+ .trim();
368
+ return prompt;
369
+ }
250
370
  const prompt = this.parts.filter(Boolean).join("\n\n").trim();
251
371
  return Promise.resolve(prompt);
252
372
  }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * PromptSectionCache - Memoizes static prompt sections across turns,
3
+ * recomputing only dynamic sections per-turn.
4
+ *
5
+ * Static sections (agent identity, glossary, knowledge base, route descriptions)
6
+ * are cached after first computation. Dynamic sections (current step context,
7
+ * directives, available tools) are recomputed on every resolveAll() call.
8
+ */
9
+
10
+ /** Section type: static sections are cached, dynamic sections recompute every turn */
11
+ export type PromptSectionType = "static" | "dynamic";
12
+
13
+ /** Configuration for prompt section caching behavior */
14
+ export interface PromptCacheConfig {
15
+ /** Whether to enable section memoization (default: true) */
16
+ enabled?: boolean;
17
+ /** Keys of sections that should always recompute every turn, even if registered as static */
18
+ volatileKeys?: string[];
19
+ }
20
+
21
+ /** Compute function that produces a section's content */
22
+ export type SectionCompute = () => string | null | Promise<string | null>;
23
+
24
+ /** Internal entry tracking a registered section */
25
+ interface PromptSectionEntry {
26
+ key: string;
27
+ type: PromptSectionType;
28
+ compute: SectionCompute;
29
+ /** undefined = not yet computed; null = computed to null; string = cached value */
30
+ cachedValue?: string | null;
31
+ }
32
+
33
+ export class PromptSectionCache {
34
+ private sections: Map<string, PromptSectionEntry> = new Map();
35
+ private insertionOrder: string[] = [];
36
+ private config: Required<PromptCacheConfig>;
37
+
38
+ constructor(config?: PromptCacheConfig) {
39
+ this.config = {
40
+ enabled: config?.enabled ?? true,
41
+ volatileKeys: config?.volatileKeys ?? [],
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Register a section with its compute function and type.
47
+ * Sections are resolved in registration order during resolveAll().
48
+ */
49
+ register(key: string, type: PromptSectionType, compute: SectionCompute): void {
50
+ const existing = this.sections.has(key);
51
+ this.sections.set(key, { key, type, compute, cachedValue: undefined });
52
+ if (!existing) {
53
+ this.insertionOrder.push(key);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get a section's value. Static sections return cached value if available;
59
+ * dynamic sections always recompute.
60
+ */
61
+ async get(key: string): Promise<string | null> {
62
+ const entry = this.sections.get(key);
63
+ if (!entry) {
64
+ return null;
65
+ }
66
+ if (this.shouldRecompute(entry)) {
67
+ const value = await entry.compute();
68
+ entry.cachedValue = value;
69
+ return value;
70
+ }
71
+ // cachedValue is defined (string or null) — return it
72
+ return entry.cachedValue!;
73
+ }
74
+
75
+ /**
76
+ * Resolve all registered sections in registration order.
77
+ * Static sections use cache when available; dynamic sections always recompute.
78
+ */
79
+ async resolveAll(): Promise<(string | null)[]> {
80
+ const results: (string | null)[] = [];
81
+ for (const key of this.insertionOrder) {
82
+ results.push(await this.get(key));
83
+ }
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * Invalidate a specific section's cache, forcing recomputation on next resolve.
89
+ */
90
+ invalidate(key: string): void {
91
+ const entry = this.sections.get(key);
92
+ if (entry) {
93
+ entry.cachedValue = undefined;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Invalidate all cached sections (e.g., on session change or /clear).
99
+ */
100
+ invalidateAll(): void {
101
+ for (const entry of this.sections.values()) {
102
+ entry.cachedValue = undefined;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check whether a section has a cached value.
108
+ */
109
+ has(key: string): boolean {
110
+ return this.sections.has(key);
111
+ }
112
+
113
+ /**
114
+ * Determine if a section needs recomputation.
115
+ */
116
+ private shouldRecompute(entry: PromptSectionEntry): boolean {
117
+ // Caching disabled — always recompute
118
+ if (!this.config.enabled) {
119
+ return true;
120
+ }
121
+ // Dynamic sections always recompute
122
+ if (entry.type === "dynamic") {
123
+ return true;
124
+ }
125
+ // Volatile keys always recompute
126
+ if (this.config.volatileKeys.includes(entry.key)) {
127
+ return true;
128
+ }
129
+ // Static section not yet computed
130
+ if (entry.cachedValue === undefined) {
131
+ return true;
132
+ }
133
+ // Static section with cached value — use cache
134
+ return false;
135
+ }
136
+ }