@falai/agent 1.1.3 → 1.2.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.
Files changed (193) 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 +4 -1
  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 -126
  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 -2
  23. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  24. package/dist/cjs/core/ResponseEngine.js +8 -8
  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 +120 -70
  28. package/dist/cjs/core/ResponseModal.js.map +1 -1
  29. package/dist/cjs/core/ResponsePipeline.d.ts +2 -1
  30. package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
  31. package/dist/cjs/core/ResponsePipeline.js +17 -19
  32. package/dist/cjs/core/ResponsePipeline.js.map +1 -1
  33. package/dist/cjs/core/RoutingEngine.d.ts +10 -0
  34. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  35. package/dist/cjs/core/RoutingEngine.js +5 -4
  36. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  37. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  38. package/dist/cjs/core/SessionManager.js +20 -0
  39. package/dist/cjs/core/SessionManager.js.map +1 -1
  40. package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
  41. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
  42. package/dist/cjs/core/StreamingToolExecutor.js +455 -0
  43. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
  44. package/dist/cjs/core/ToolManager.d.ts +18 -1
  45. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  46. package/dist/cjs/core/ToolManager.js +91 -0
  47. package/dist/cjs/core/ToolManager.js.map +1 -1
  48. package/dist/cjs/index.d.ts +5 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +8 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/providers/AnthropicProvider.d.ts +7 -0
  53. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  54. package/dist/cjs/providers/AnthropicProvider.js +109 -19
  55. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  56. package/dist/cjs/providers/GeminiProvider.d.ts +32 -0
  57. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  58. package/dist/cjs/providers/GeminiProvider.js +160 -53
  59. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  60. package/dist/cjs/providers/OpenAIProvider.d.ts +5 -0
  61. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  62. package/dist/cjs/providers/OpenAIProvider.js +65 -18
  63. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  64. package/dist/cjs/providers/OpenRouterProvider.d.ts +5 -0
  65. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  66. package/dist/cjs/providers/OpenRouterProvider.js +57 -18
  67. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  68. package/dist/cjs/types/agent.d.ts +44 -0
  69. package/dist/cjs/types/agent.d.ts.map +1 -1
  70. package/dist/cjs/types/agent.js.map +1 -1
  71. package/dist/cjs/types/ai.d.ts +2 -2
  72. package/dist/cjs/types/ai.d.ts.map +1 -1
  73. package/dist/cjs/types/compaction.d.ts +50 -0
  74. package/dist/cjs/types/compaction.d.ts.map +1 -0
  75. package/dist/cjs/types/compaction.js +6 -0
  76. package/dist/cjs/types/compaction.js.map +1 -0
  77. package/dist/cjs/types/index.d.ts +4 -2
  78. package/dist/cjs/types/index.d.ts.map +1 -1
  79. package/dist/cjs/types/index.js.map +1 -1
  80. package/dist/cjs/types/tool.d.ts +84 -0
  81. package/dist/cjs/types/tool.d.ts.map +1 -1
  82. package/dist/core/Agent.d.ts +17 -1
  83. package/dist/core/Agent.d.ts.map +1 -1
  84. package/dist/core/Agent.js +47 -0
  85. package/dist/core/Agent.js.map +1 -1
  86. package/dist/core/BatchPromptBuilder.d.ts +3 -0
  87. package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
  88. package/dist/core/BatchPromptBuilder.js +4 -1
  89. package/dist/core/BatchPromptBuilder.js.map +1 -1
  90. package/dist/core/CompactionEngine.d.ts +65 -0
  91. package/dist/core/CompactionEngine.d.ts.map +1 -0
  92. package/dist/core/CompactionEngine.js +244 -0
  93. package/dist/core/CompactionEngine.js.map +1 -0
  94. package/dist/core/PromptComposer.d.ts +8 -1
  95. package/dist/core/PromptComposer.d.ts.map +1 -1
  96. package/dist/core/PromptComposer.js +238 -126
  97. package/dist/core/PromptComposer.js.map +1 -1
  98. package/dist/core/PromptSectionCache.d.ts +57 -0
  99. package/dist/core/PromptSectionCache.d.ts.map +1 -0
  100. package/dist/core/PromptSectionCache.js +104 -0
  101. package/dist/core/PromptSectionCache.js.map +1 -0
  102. package/dist/core/ResponseEngine.d.ts +3 -2
  103. package/dist/core/ResponseEngine.d.ts.map +1 -1
  104. package/dist/core/ResponseEngine.js +8 -8
  105. package/dist/core/ResponseEngine.js.map +1 -1
  106. package/dist/core/ResponseModal.d.ts.map +1 -1
  107. package/dist/core/ResponseModal.js +121 -71
  108. package/dist/core/ResponseModal.js.map +1 -1
  109. package/dist/core/ResponsePipeline.d.ts +2 -1
  110. package/dist/core/ResponsePipeline.d.ts.map +1 -1
  111. package/dist/core/ResponsePipeline.js +18 -20
  112. package/dist/core/ResponsePipeline.js.map +1 -1
  113. package/dist/core/RoutingEngine.d.ts +10 -0
  114. package/dist/core/RoutingEngine.d.ts.map +1 -1
  115. package/dist/core/RoutingEngine.js +6 -5
  116. package/dist/core/RoutingEngine.js.map +1 -1
  117. package/dist/core/SessionManager.d.ts.map +1 -1
  118. package/dist/core/SessionManager.js +17 -0
  119. package/dist/core/SessionManager.js.map +1 -1
  120. package/dist/core/StreamingToolExecutor.d.ts +142 -0
  121. package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
  122. package/dist/core/StreamingToolExecutor.js +448 -0
  123. package/dist/core/StreamingToolExecutor.js.map +1 -0
  124. package/dist/core/ToolManager.d.ts +18 -1
  125. package/dist/core/ToolManager.d.ts.map +1 -1
  126. package/dist/core/ToolManager.js +91 -0
  127. package/dist/core/ToolManager.js.map +1 -1
  128. package/dist/index.d.ts +5 -1
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +3 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/providers/AnthropicProvider.d.ts +7 -0
  133. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  134. package/dist/providers/AnthropicProvider.js +109 -19
  135. package/dist/providers/AnthropicProvider.js.map +1 -1
  136. package/dist/providers/GeminiProvider.d.ts +32 -0
  137. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  138. package/dist/providers/GeminiProvider.js +160 -53
  139. package/dist/providers/GeminiProvider.js.map +1 -1
  140. package/dist/providers/OpenAIProvider.d.ts +5 -0
  141. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  142. package/dist/providers/OpenAIProvider.js +65 -18
  143. package/dist/providers/OpenAIProvider.js.map +1 -1
  144. package/dist/providers/OpenRouterProvider.d.ts +5 -0
  145. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  146. package/dist/providers/OpenRouterProvider.js +57 -18
  147. package/dist/providers/OpenRouterProvider.js.map +1 -1
  148. package/dist/types/agent.d.ts +44 -0
  149. package/dist/types/agent.d.ts.map +1 -1
  150. package/dist/types/agent.js.map +1 -1
  151. package/dist/types/ai.d.ts +2 -2
  152. package/dist/types/ai.d.ts.map +1 -1
  153. package/dist/types/compaction.d.ts +50 -0
  154. package/dist/types/compaction.d.ts.map +1 -0
  155. package/dist/types/compaction.js +5 -0
  156. package/dist/types/compaction.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -2
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/types/index.js.map +1 -1
  160. package/dist/types/tool.d.ts +84 -0
  161. package/dist/types/tool.d.ts.map +1 -1
  162. package/docs/api/overview.md +140 -0
  163. package/docs/core/tools/enhanced-tool.md +186 -0
  164. package/docs/core/tools/streaming-execution.md +161 -0
  165. package/docs/guides/context-compaction.md +96 -0
  166. package/docs/guides/prompt-optimization.md +164 -0
  167. package/examples/advanced-patterns/context-compaction.ts +223 -0
  168. package/examples/advanced-patterns/streaming-responses.ts +85 -7
  169. package/examples/tools/enhanced-tool-metadata.ts +268 -0
  170. package/examples/tools/streaming-tool-execution.ts +283 -0
  171. package/package.json +1 -1
  172. package/src/core/Agent.ts +58 -2
  173. package/src/core/BatchPromptBuilder.ts +4 -1
  174. package/src/core/CompactionEngine.ts +318 -0
  175. package/src/core/PromptComposer.ts +259 -156
  176. package/src/core/PromptSectionCache.ts +136 -0
  177. package/src/core/ResponseEngine.ts +7 -11
  178. package/src/core/ResponseModal.ts +133 -83
  179. package/src/core/ResponsePipeline.ts +22 -22
  180. package/src/core/RoutingEngine.ts +16 -5
  181. package/src/core/SessionManager.ts +19 -0
  182. package/src/core/StreamingToolExecutor.ts +572 -0
  183. package/src/core/ToolManager.ts +151 -41
  184. package/src/index.ts +14 -0
  185. package/src/providers/AnthropicProvider.ts +121 -24
  186. package/src/providers/GeminiProvider.ts +174 -54
  187. package/src/providers/OpenAIProvider.ts +77 -25
  188. package/src/providers/OpenRouterProvider.ts +68 -25
  189. package/src/types/agent.ts +45 -0
  190. package/src/types/ai.ts +2 -2
  191. package/src/types/compaction.ts +52 -0
  192. package/src/types/index.ts +35 -14
  193. package/src/types/tool.ts +108 -0
@@ -3,58 +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 Identity");
20
- lines.push(
21
- `You are "${agent.name}". Always refer to yourself by this name.`
22
- );
23
- if (agent.identity) {
24
- lines.push(await render(agent.identity, this.renderContext));
25
- }
26
- if (agent.personality) {
27
- lines.push(
28
- `Communicate in the following style: ${await render(
29
- agent.personality,
30
- this.renderContext
31
- )}`
32
- );
33
- }
34
- if (agent.goal) {
35
- lines.push(`Your primary goal: ${agent.goal}`);
36
- }
37
- if (agent.description) {
38
- lines.push(`About you: ${agent.description}`);
39
- }
40
- if (agent.rules?.length) {
41
- const renderedRules = await renderMany(agent.rules, this.renderContext);
42
- lines.push(
43
- `You MUST always follow these rules:\n- ${renderedRules.join("\n- ")}`
44
- );
45
- }
46
- if (agent.prohibitions?.length) {
47
- const renderedProhibitions = await renderMany(
48
- agent.prohibitions,
49
- this.renderContext
50
- );
25
+ const compute = async (): Promise<string | null> => {
26
+ const lines: string[] = [];
27
+ lines.push("## Agent Identity");
51
28
  lines.push(
52
- `You MUST NEVER do the following:\n- ${renderedProhibitions.join(
53
- "\n- "
54
- )}`
29
+ `You are "${agent.name}". Always refer to yourself by this name.`
55
30
  );
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,
57
+ this.renderContext
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);
56
73
  }
57
- this.parts.push(lines.join("\n"));
58
74
  return this;
59
75
  }
60
76
 
@@ -63,58 +79,99 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
63
79
  }
64
80
 
65
81
  async addScoringRules(): Promise<this> {
66
- this.parts.push(
67
- `## Scoring Rules\n\n${[
82
+ const compute = (): string | null => {
83
+ return `## Scoring Rules\n\n${[
68
84
  "- 90-100: explicit keywords + clear intent",
69
85
  "- 70-89: strong contextual evidence + relevant keywords",
70
86
  "- 50-69: moderate relevance",
71
87
  "- 30-49: weak connection or ambiguous",
72
88
  "- 0-29: minimal/none",
73
89
  "Return ONLY JSON matching the provided schema. Include scores for ALL routes.",
74
- ].join("\n")}`
75
- );
90
+ ].join("\n")}`;
91
+ };
92
+
93
+ if (this.cache) {
94
+ this.cache.register("scoringRules", "static", compute);
95
+ } else {
96
+ this.parts.push(compute()!);
97
+ }
76
98
  return Promise.resolve(this);
77
99
  }
78
100
 
79
101
  async addInstruction(text: string): Promise<this> {
80
- 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
+ }
81
112
  return Promise.resolve(this);
82
113
  }
83
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
+ */
84
119
  async addInteractionHistory(history: Event[], note?: string): Promise<this> {
85
- const recent = history
86
- .slice(-10)
87
- .map((e) => `- ${JSON.stringify(e)}`)
88
- .join("\n");
89
- const header = note ? `${note}\n\n` : "";
90
- this.parts.push(
91
- `## Interaction History\n\n${header}Recent conversation events:\n\n${recent}`
92
- );
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
+ }
93
134
  return Promise.resolve(this);
94
135
  }
95
136
 
96
137
  async addLastMessage(message: string): Promise<this> {
97
- 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
+ }
98
147
  return Promise.resolve(this);
99
148
  }
100
149
 
101
150
  async addGlossary(terms: Term<TContext>[]): Promise<this> {
102
151
  if (!terms.length) return this;
103
152
 
104
- const renderedTerms = await Promise.all(
105
- terms.map(async (t) => {
106
- const name = await render(t.name, this.renderContext);
107
- const description = await render(t.description, this.renderContext);
108
- const synonyms = t.synonyms
109
- ? await renderMany(t.synonyms, this.renderContext)
110
- : [];
111
- const synonymText =
112
- synonyms.length > 0 ? ` (synonyms: ${synonyms.join(", ")})` : "";
113
- return `- **${name}**${synonymText}: ${description}`;
114
- })
115
- );
116
-
117
- 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
+ }
118
175
  return this;
119
176
  }
120
177
 
@@ -122,56 +179,64 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
122
179
  const enabled = guidelines.filter((g) => g.enabled !== false);
123
180
  if (!enabled.length) return this;
124
181
 
125
- const evaluator = new ConditionEvaluator(this.renderContext);
126
- const activeGuidelines: Guideline<TContext, TData>[] = [];
127
- const allAIContextStrings: string[] = [];
182
+ const compute = async (): Promise<string | null> => {
183
+ const evaluator = new ConditionEvaluator(this.renderContext);
184
+ const activeGuidelines: Guideline<TContext, TData>[] = [];
185
+ const allAIContextStrings: string[] = [];
128
186
 
129
- // Evaluate guideline conditions to determine which are active
130
- for (const guideline of enabled) {
131
- if (guideline.condition) {
132
- const evaluation = await evaluator.evaluateCondition(guideline.condition, 'AND');
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');
133
191
 
134
- // Collect AI context strings for prompt
135
- allAIContextStrings.push(...evaluation.aiContextStrings);
192
+ // Collect AI context strings for prompt
193
+ allAIContextStrings.push(...evaluation.aiContextStrings);
136
194
 
137
- // Include guideline if:
138
- // 1. No programmatic conditions (only strings) - always active
139
- // 2. Programmatic conditions evaluate to true
140
- if (!evaluation.hasProgrammaticConditions || evaluation.programmaticResult) {
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
141
203
  activeGuidelines.push(guideline);
142
204
  }
143
- } else {
144
- // No condition means always active
145
- activeGuidelines.push(guideline);
146
205
  }
147
- }
148
206
 
149
- if (!activeGuidelines.length && !allAIContextStrings.length) return this;
150
-
151
- const renderedGuidelines = await Promise.all(
152
- activeGuidelines.map(async (g, i) => {
153
- const action = await render(g.action, this.renderContext);
154
- if (g.condition) {
155
- // Use AI context strings if available, otherwise render the condition
156
- const conditionStrings = extractAIContextStrings(g.condition);
157
- if (conditionStrings.length > 0) {
158
- const conditionText = conditionStrings.join(" AND ");
159
- 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
+ }
160
219
  }
161
- }
162
- return `- Guideline #${i + 1}: ${action}`;
163
- })
164
- );
165
-
166
- // Add any additional AI context from inactive guidelines
167
- if (allAIContextStrings.length > 0) {
168
- const uniqueContextStrings = Array.from(new Set(allAIContextStrings));
169
- const contextSection = `\n\n**Additional Context:** ${uniqueContextStrings.join(", ")}`;
170
- 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);
171
236
  } else {
172
- this.parts.push(`## Guidelines\n\n${renderedGuidelines.join("\n")}`);
237
+ const result = await compute();
238
+ if (result) this.parts.push(result);
173
239
  }
174
-
175
240
  return this;
176
241
  }
177
242
 
@@ -179,65 +244,88 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
179
244
  agentKnowledgeBase?: Record<string, unknown>,
180
245
  routeKnowledgeBase?: Record<string, unknown>
181
246
  ): Promise<this> {
182
- // Merge agent and route knowledge bases (route takes precedence for conflicts)
183
- const mergedKnowledge = {
184
- ...(agentKnowledgeBase || {}),
185
- ...(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;
186
259
  };
187
260
 
188
- // Only add section if there's knowledge data
189
- if (Object.keys(mergedKnowledge).length > 0) {
190
- const formatted = formatKnowledgeBase(mergedKnowledge, "Knowledge Base");
191
- 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);
192
266
  }
193
-
194
267
  return Promise.resolve(this);
195
268
  }
196
269
 
197
270
  async addActiveRoutes(routes: Route<TContext, TData>[]): Promise<this> {
198
271
  if (!routes.length) return this;
199
272
 
200
- const renderedRoutes = await Promise.all(
201
- routes.map(async (r, i) => {
202
- const whenContextStrings = r.when ? extractAIContextStrings(r.when) : [];
203
- const conditions =
204
- whenContextStrings.length > 0
205
- ? `\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}`
206
283
  : "";
207
- const desc = r.description
208
- ? `\n\n **Description:** ${r.description}`
209
- : "";
210
- const rules = await renderMany(r.getRules(), this.renderContext);
211
- const prohibitions = await renderMany(
212
- r.getProhibitions(),
213
- this.renderContext
214
- );
215
- const rulesInfo =
216
- rules.length > 0
217
- ? `\n\n **Rules:**\n ${rules.map((x) => ` - ${x}`).join("\n ")}`
218
- : "";
219
- const prohibitionsInfo =
220
- prohibitions.length > 0
221
- ? `\n\n **Prohibitions:**\n ${prohibitions
222
- .map((x) => ` - ${x}`)
223
- .join("\n ")}`
224
- : "";
225
- return `### Route ${i + 1}: ${r.title
226
- }${desc}${conditions}${rulesInfo}${prohibitionsInfo}`;
227
- })
228
- );
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
+ };
229
305
 
230
- 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
+ }
231
312
  return this;
232
313
  }
233
314
 
234
315
  async addDirectives(directives?: string[]): Promise<this> {
235
316
  if (!directives?.length) return this;
236
- this.parts.push(
237
- `## Directives\n\nAddress concisely:\n\n${directives
317
+
318
+ const compute = (): string | null => {
319
+ return `## Directives\n\nAddress concisely:\n\n${directives
238
320
  .map((d) => `- ${d}`)
239
- .join("\n")}`
240
- );
321
+ .join("\n")}`;
322
+ };
323
+
324
+ if (this.cache) {
325
+ this.cache.register("directives", "dynamic", compute);
326
+ } else {
327
+ this.parts.push(compute()!);
328
+ }
241
329
  return Promise.resolve(this);
242
330
  }
243
331
 
@@ -251,19 +339,34 @@ export class PromptComposer<TContext = unknown, TData = unknown> {
251
339
  ): Promise<this> {
252
340
  if (!tools?.length) return this;
253
341
 
254
- const renderedTools = tools.map((tool, i) => {
255
- const toolName = tool.name || tool.id;
256
- const desc = tool.description
257
- ? `\n Description: ${tool.description}`
258
- : "";
259
- return `### Tool ${i + 1}: ${toolName}${desc}`;
260
- });
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
+ };
261
352
 
262
- 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
+ }
263
358
  return Promise.resolve(this);
264
359
  }
265
360
 
266
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
+ }
267
370
  const prompt = this.parts.filter(Boolean).join("\n\n").trim();
268
371
  return Promise.resolve(prompt);
269
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
+ }