@tanstack/ai 0.6.3 → 0.8.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 (68) hide show
  1. package/README.md +6 -6
  2. package/dist/esm/activities/chat/index.d.ts +20 -0
  3. package/dist/esm/activities/chat/index.js +248 -213
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/middleware/compose.d.ts +66 -0
  6. package/dist/esm/activities/chat/middleware/compose.js +327 -0
  7. package/dist/esm/activities/chat/middleware/compose.js.map +1 -0
  8. package/dist/esm/activities/chat/middleware/index.d.ts +2 -0
  9. package/dist/esm/activities/chat/middleware/tool-cache-middleware.d.ts +89 -0
  10. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +76 -0
  11. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -0
  12. package/dist/esm/activities/chat/middleware/types.d.ts +307 -0
  13. package/dist/esm/activities/chat/stream/processor.d.ts +64 -40
  14. package/dist/esm/activities/chat/stream/processor.js +466 -218
  15. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/types.d.ts +17 -0
  17. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -1
  18. package/dist/esm/activities/chat/tools/tool-calls.js +148 -64
  19. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  20. package/dist/esm/activities/generateImage/index.js +1 -1
  21. package/dist/esm/activities/generateImage/index.js.map +1 -1
  22. package/dist/esm/activities/generateSpeech/index.js +1 -1
  23. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  24. package/dist/esm/activities/generateTranscription/index.js +1 -1
  25. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  26. package/dist/esm/activities/generateVideo/index.js +1 -1
  27. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  28. package/dist/esm/activities/summarize/index.js +1 -1
  29. package/dist/esm/activities/summarize/index.js.map +1 -1
  30. package/dist/esm/index.d.ts +3 -1
  31. package/dist/esm/index.js +2 -2
  32. package/dist/esm/middlewares/content-guard.d.ts +77 -0
  33. package/dist/esm/middlewares/content-guard.js +155 -0
  34. package/dist/esm/middlewares/content-guard.js.map +1 -0
  35. package/dist/esm/middlewares/index.d.ts +2 -0
  36. package/dist/esm/middlewares/index.js +7 -0
  37. package/dist/esm/middlewares/index.js.map +1 -0
  38. package/dist/esm/middlewares/tool-cache.d.ts +1 -0
  39. package/dist/esm/realtime/index.d.ts +30 -0
  40. package/dist/esm/realtime/index.js +8 -0
  41. package/dist/esm/realtime/index.js.map +1 -0
  42. package/dist/esm/realtime/types.d.ts +234 -0
  43. package/dist/esm/types.d.ts +18 -4
  44. package/package.json +6 -6
  45. package/src/activities/chat/index.ts +322 -256
  46. package/src/activities/chat/middleware/compose.ts +392 -0
  47. package/src/activities/chat/middleware/index.ts +17 -0
  48. package/src/activities/chat/middleware/tool-cache-middleware.ts +189 -0
  49. package/src/activities/chat/middleware/types.ts +419 -0
  50. package/src/activities/chat/stream/processor.ts +630 -259
  51. package/src/activities/chat/stream/types.ts +18 -0
  52. package/src/activities/chat/tools/tool-calls.ts +225 -87
  53. package/src/activities/generateImage/index.ts +1 -1
  54. package/src/activities/generateSpeech/index.ts +1 -1
  55. package/src/activities/generateTranscription/index.ts +1 -1
  56. package/src/activities/generateVideo/index.ts +1 -1
  57. package/src/activities/summarize/index.ts +1 -1
  58. package/src/index.ts +41 -2
  59. package/src/middlewares/content-guard.ts +285 -0
  60. package/src/middlewares/index.ts +13 -0
  61. package/src/middlewares/tool-cache.ts +6 -0
  62. package/src/realtime/index.ts +38 -0
  63. package/src/realtime/types.ts +294 -0
  64. package/src/types.ts +19 -2
  65. package/dist/esm/event-client.d.ts +0 -394
  66. package/dist/esm/event-client.js +0 -13
  67. package/dist/esm/event-client.js.map +0 -1
  68. package/src/event-client.ts +0 -497
@@ -0,0 +1,327 @@
1
+ import { aiEventClient } from "@tanstack/ai-event-client";
2
+ function shouldSkipInstrumentation(mw) {
3
+ return mw.name === "devtools";
4
+ }
5
+ function instrumentCtx(ctx) {
6
+ return {
7
+ requestId: ctx.requestId,
8
+ streamId: ctx.streamId,
9
+ clientId: ctx.conversationId,
10
+ timestamp: Date.now()
11
+ };
12
+ }
13
+ class MiddlewareRunner {
14
+ constructor(middlewares) {
15
+ this.middlewares = middlewares;
16
+ }
17
+ get hasMiddleware() {
18
+ return this.middlewares.length > 0;
19
+ }
20
+ /**
21
+ * Pipe config through all middleware onConfig hooks in order.
22
+ * Each middleware receives the merged config from previous middleware.
23
+ * Partial returns are shallow-merged with the current config.
24
+ */
25
+ async runOnConfig(ctx, config) {
26
+ let current = config;
27
+ for (const mw of this.middlewares) {
28
+ if (mw.onConfig) {
29
+ const skip = shouldSkipInstrumentation(mw);
30
+ const start = Date.now();
31
+ const result = await mw.onConfig(ctx, current);
32
+ const hasTransform = result !== void 0 && result !== null;
33
+ if (hasTransform) {
34
+ current = { ...current, ...result };
35
+ }
36
+ if (!skip) {
37
+ const base = instrumentCtx(ctx);
38
+ aiEventClient.emit("middleware:hook:executed", {
39
+ ...base,
40
+ middlewareName: mw.name || "unnamed",
41
+ hookName: "onConfig",
42
+ iteration: ctx.iteration,
43
+ duration: Date.now() - start,
44
+ hasTransform
45
+ });
46
+ if (hasTransform) {
47
+ aiEventClient.emit("middleware:config:transformed", {
48
+ ...base,
49
+ middlewareName: mw.name || "unnamed",
50
+ iteration: ctx.iteration,
51
+ changes: result
52
+ });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return current;
58
+ }
59
+ /**
60
+ * Call onStart on all middleware in order.
61
+ */
62
+ async runOnStart(ctx) {
63
+ for (const mw of this.middlewares) {
64
+ if (mw.onStart) {
65
+ const skip = shouldSkipInstrumentation(mw);
66
+ const start = Date.now();
67
+ await mw.onStart(ctx);
68
+ if (!skip) {
69
+ aiEventClient.emit("middleware:hook:executed", {
70
+ ...instrumentCtx(ctx),
71
+ middlewareName: mw.name || "unnamed",
72
+ hookName: "onStart",
73
+ iteration: ctx.iteration,
74
+ duration: Date.now() - start,
75
+ hasTransform: false
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Pipe a single chunk through all middleware onChunk hooks in order.
83
+ * Returns the resulting chunks (0..N) to yield to the consumer.
84
+ *
85
+ * - void: pass through unchanged
86
+ * - chunk: replace with this chunk
87
+ * - chunk[]: expand to multiple chunks
88
+ * - null: drop the chunk entirely
89
+ */
90
+ async runOnChunk(ctx, chunk) {
91
+ let chunks = [chunk];
92
+ for (const mw of this.middlewares) {
93
+ if (!mw.onChunk) continue;
94
+ const skip = shouldSkipInstrumentation(mw);
95
+ const nextChunks = [];
96
+ for (const c of chunks) {
97
+ const result = await mw.onChunk(ctx, c);
98
+ if (result === null) {
99
+ if (!skip) {
100
+ aiEventClient.emit("middleware:chunk:transformed", {
101
+ ...instrumentCtx(ctx),
102
+ middlewareName: mw.name || "unnamed",
103
+ originalChunkType: c.type,
104
+ resultCount: 0,
105
+ wasDropped: true
106
+ });
107
+ }
108
+ continue;
109
+ } else if (result === void 0) {
110
+ nextChunks.push(c);
111
+ } else if (Array.isArray(result)) {
112
+ nextChunks.push(...result);
113
+ if (!skip) {
114
+ aiEventClient.emit("middleware:chunk:transformed", {
115
+ ...instrumentCtx(ctx),
116
+ middlewareName: mw.name || "unnamed",
117
+ originalChunkType: c.type,
118
+ resultCount: result.length,
119
+ wasDropped: false
120
+ });
121
+ }
122
+ } else {
123
+ nextChunks.push(result);
124
+ if (!skip) {
125
+ aiEventClient.emit("middleware:chunk:transformed", {
126
+ ...instrumentCtx(ctx),
127
+ middlewareName: mw.name || "unnamed",
128
+ originalChunkType: c.type,
129
+ resultCount: 1,
130
+ wasDropped: false
131
+ });
132
+ }
133
+ }
134
+ }
135
+ chunks = nextChunks;
136
+ }
137
+ return chunks;
138
+ }
139
+ /**
140
+ * Run onBeforeToolCall through middleware in order.
141
+ * Returns the first non-void decision, or undefined to continue normally.
142
+ */
143
+ async runOnBeforeToolCall(ctx, hookCtx) {
144
+ for (const mw of this.middlewares) {
145
+ if (mw.onBeforeToolCall) {
146
+ const skip = shouldSkipInstrumentation(mw);
147
+ const start = Date.now();
148
+ const decision = await mw.onBeforeToolCall(ctx, hookCtx);
149
+ const hasTransform = decision !== void 0 && decision !== null;
150
+ if (!skip) {
151
+ aiEventClient.emit("middleware:hook:executed", {
152
+ ...instrumentCtx(ctx),
153
+ middlewareName: mw.name || "unnamed",
154
+ hookName: "onBeforeToolCall",
155
+ iteration: ctx.iteration,
156
+ duration: Date.now() - start,
157
+ hasTransform
158
+ });
159
+ }
160
+ if (hasTransform) {
161
+ return decision;
162
+ }
163
+ }
164
+ }
165
+ return void 0;
166
+ }
167
+ /**
168
+ * Run onAfterToolCall on all middleware in order.
169
+ */
170
+ async runOnAfterToolCall(ctx, info) {
171
+ for (const mw of this.middlewares) {
172
+ if (mw.onAfterToolCall) {
173
+ const skip = shouldSkipInstrumentation(mw);
174
+ const start = Date.now();
175
+ await mw.onAfterToolCall(ctx, info);
176
+ if (!skip) {
177
+ aiEventClient.emit("middleware:hook:executed", {
178
+ ...instrumentCtx(ctx),
179
+ middlewareName: mw.name || "unnamed",
180
+ hookName: "onAfterToolCall",
181
+ iteration: ctx.iteration,
182
+ duration: Date.now() - start,
183
+ hasTransform: false
184
+ });
185
+ }
186
+ }
187
+ }
188
+ }
189
+ /**
190
+ * Run onUsage on all middleware in order.
191
+ */
192
+ async runOnUsage(ctx, usage) {
193
+ for (const mw of this.middlewares) {
194
+ if (mw.onUsage) {
195
+ const skip = shouldSkipInstrumentation(mw);
196
+ const start = Date.now();
197
+ await mw.onUsage(ctx, usage);
198
+ if (!skip) {
199
+ aiEventClient.emit("middleware:hook:executed", {
200
+ ...instrumentCtx(ctx),
201
+ middlewareName: mw.name || "unnamed",
202
+ hookName: "onUsage",
203
+ iteration: ctx.iteration,
204
+ duration: Date.now() - start,
205
+ hasTransform: false
206
+ });
207
+ }
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * Run onFinish on all middleware in order.
213
+ */
214
+ async runOnFinish(ctx, info) {
215
+ for (const mw of this.middlewares) {
216
+ if (mw.onFinish) {
217
+ const skip = shouldSkipInstrumentation(mw);
218
+ const start = Date.now();
219
+ await mw.onFinish(ctx, info);
220
+ if (!skip) {
221
+ aiEventClient.emit("middleware:hook:executed", {
222
+ ...instrumentCtx(ctx),
223
+ middlewareName: mw.name || "unnamed",
224
+ hookName: "onFinish",
225
+ iteration: ctx.iteration,
226
+ duration: Date.now() - start,
227
+ hasTransform: false
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+ /**
234
+ * Run onAbort on all middleware in order.
235
+ */
236
+ async runOnAbort(ctx, info) {
237
+ for (const mw of this.middlewares) {
238
+ if (mw.onAbort) {
239
+ const skip = shouldSkipInstrumentation(mw);
240
+ const start = Date.now();
241
+ await mw.onAbort(ctx, info);
242
+ if (!skip) {
243
+ aiEventClient.emit("middleware:hook:executed", {
244
+ ...instrumentCtx(ctx),
245
+ middlewareName: mw.name || "unnamed",
246
+ hookName: "onAbort",
247
+ iteration: ctx.iteration,
248
+ duration: Date.now() - start,
249
+ hasTransform: false
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Run onError on all middleware in order.
257
+ */
258
+ async runOnError(ctx, info) {
259
+ for (const mw of this.middlewares) {
260
+ if (mw.onError) {
261
+ const skip = shouldSkipInstrumentation(mw);
262
+ const start = Date.now();
263
+ await mw.onError(ctx, info);
264
+ if (!skip) {
265
+ aiEventClient.emit("middleware:hook:executed", {
266
+ ...instrumentCtx(ctx),
267
+ middlewareName: mw.name || "unnamed",
268
+ hookName: "onError",
269
+ iteration: ctx.iteration,
270
+ duration: Date.now() - start,
271
+ hasTransform: false
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ /**
278
+ * Run onIteration on all middleware in order.
279
+ * Called at the start of each agent loop iteration.
280
+ */
281
+ async runOnIteration(ctx, info) {
282
+ for (const mw of this.middlewares) {
283
+ if (mw.onIteration) {
284
+ const skip = shouldSkipInstrumentation(mw);
285
+ const start = Date.now();
286
+ await mw.onIteration(ctx, info);
287
+ if (!skip) {
288
+ aiEventClient.emit("middleware:hook:executed", {
289
+ ...instrumentCtx(ctx),
290
+ middlewareName: mw.name || "unnamed",
291
+ hookName: "onIteration",
292
+ iteration: ctx.iteration,
293
+ duration: Date.now() - start,
294
+ hasTransform: false
295
+ });
296
+ }
297
+ }
298
+ }
299
+ }
300
+ /**
301
+ * Run onToolPhaseComplete on all middleware in order.
302
+ * Called after all tool calls in an iteration have been processed.
303
+ */
304
+ async runOnToolPhaseComplete(ctx, info) {
305
+ for (const mw of this.middlewares) {
306
+ if (mw.onToolPhaseComplete) {
307
+ const skip = shouldSkipInstrumentation(mw);
308
+ const start = Date.now();
309
+ await mw.onToolPhaseComplete(ctx, info);
310
+ if (!skip) {
311
+ aiEventClient.emit("middleware:hook:executed", {
312
+ ...instrumentCtx(ctx),
313
+ middlewareName: mw.name || "unnamed",
314
+ hookName: "onToolPhaseComplete",
315
+ iteration: ctx.iteration,
316
+ duration: Date.now() - start,
317
+ hasTransform: false
318
+ });
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ export {
325
+ MiddlewareRunner
326
+ };
327
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sources":["../../../../../src/activities/chat/middleware/compose.ts"],"sourcesContent":["import { aiEventClient } from '@tanstack/ai-event-client'\nimport type { StreamChunk } from '../../../types'\nimport type {\n AbortInfo,\n AfterToolCallInfo,\n BeforeToolCallDecision,\n ChatMiddleware,\n ChatMiddlewareConfig,\n ChatMiddlewareContext,\n ErrorInfo,\n FinishInfo,\n IterationInfo,\n ToolCallHookContext,\n ToolPhaseCompleteInfo,\n UsageInfo,\n} from './types'\n\n/** Check if a middleware should be skipped for instrumentation events. */\nfunction shouldSkipInstrumentation(mw: ChatMiddleware): boolean {\n return mw.name === 'devtools'\n}\n\n/** Build the base context for middleware instrumentation events. */\nfunction instrumentCtx(ctx: ChatMiddlewareContext) {\n return {\n requestId: ctx.requestId,\n streamId: ctx.streamId,\n clientId: ctx.conversationId,\n timestamp: Date.now(),\n }\n}\n\n/**\n * Internal middleware runner that manages composed execution of middleware hooks.\n * Created once per chat() invocation.\n */\nexport class MiddlewareRunner {\n private readonly middlewares: ReadonlyArray<ChatMiddleware>\n\n constructor(middlewares: ReadonlyArray<ChatMiddleware>) {\n this.middlewares = middlewares\n }\n\n get hasMiddleware(): boolean {\n return this.middlewares.length > 0\n }\n\n /**\n * Pipe config through all middleware onConfig hooks in order.\n * Each middleware receives the merged config from previous middleware.\n * Partial returns are shallow-merged with the current config.\n */\n async runOnConfig(\n ctx: ChatMiddlewareContext,\n config: ChatMiddlewareConfig,\n ): Promise<ChatMiddlewareConfig> {\n let current = config\n for (const mw of this.middlewares) {\n if (mw.onConfig) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n const result = await mw.onConfig(ctx, current)\n const hasTransform = result !== undefined && result !== null\n if (hasTransform) {\n current = { ...current, ...result }\n }\n if (!skip) {\n const base = instrumentCtx(ctx)\n aiEventClient.emit('middleware:hook:executed', {\n ...base,\n middlewareName: mw.name || 'unnamed',\n hookName: 'onConfig',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform,\n })\n if (hasTransform) {\n aiEventClient.emit('middleware:config:transformed', {\n ...base,\n middlewareName: mw.name || 'unnamed',\n iteration: ctx.iteration,\n changes: result as Record<string, unknown>,\n })\n }\n }\n }\n }\n return current\n }\n\n /**\n * Call onStart on all middleware in order.\n */\n async runOnStart(ctx: ChatMiddlewareContext): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onStart) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onStart(ctx)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onStart',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Pipe a single chunk through all middleware onChunk hooks in order.\n * Returns the resulting chunks (0..N) to yield to the consumer.\n *\n * - void: pass through unchanged\n * - chunk: replace with this chunk\n * - chunk[]: expand to multiple chunks\n * - null: drop the chunk entirely\n */\n async runOnChunk(\n ctx: ChatMiddlewareContext,\n chunk: StreamChunk,\n ): Promise<Array<StreamChunk>> {\n let chunks: Array<StreamChunk> = [chunk]\n\n for (const mw of this.middlewares) {\n if (!mw.onChunk) continue\n const skip = shouldSkipInstrumentation(mw)\n\n const nextChunks: Array<StreamChunk> = []\n for (const c of chunks) {\n const result = await mw.onChunk(ctx, c)\n if (result === null) {\n // Drop this chunk\n if (!skip) {\n aiEventClient.emit('middleware:chunk:transformed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n originalChunkType: c.type,\n resultCount: 0,\n wasDropped: true,\n })\n }\n continue\n } else if (result === undefined) {\n // Pass through — no instrumentation for pass-throughs\n nextChunks.push(c)\n } else if (Array.isArray(result)) {\n // Expand\n nextChunks.push(...result)\n if (!skip) {\n aiEventClient.emit('middleware:chunk:transformed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n originalChunkType: c.type,\n resultCount: result.length,\n wasDropped: false,\n })\n }\n } else {\n // Replace\n nextChunks.push(result)\n if (!skip) {\n aiEventClient.emit('middleware:chunk:transformed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n originalChunkType: c.type,\n resultCount: 1,\n wasDropped: false,\n })\n }\n }\n }\n chunks = nextChunks\n }\n\n return chunks\n }\n\n /**\n * Run onBeforeToolCall through middleware in order.\n * Returns the first non-void decision, or undefined to continue normally.\n */\n async runOnBeforeToolCall(\n ctx: ChatMiddlewareContext,\n hookCtx: ToolCallHookContext,\n ): Promise<BeforeToolCallDecision> {\n for (const mw of this.middlewares) {\n if (mw.onBeforeToolCall) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n const decision = await mw.onBeforeToolCall(ctx, hookCtx)\n const hasTransform = decision !== undefined && decision !== null\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onBeforeToolCall',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform,\n })\n }\n if (hasTransform) {\n return decision\n }\n }\n }\n return undefined\n }\n\n /**\n * Run onAfterToolCall on all middleware in order.\n */\n async runOnAfterToolCall(\n ctx: ChatMiddlewareContext,\n info: AfterToolCallInfo,\n ): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onAfterToolCall) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onAfterToolCall(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onAfterToolCall',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onUsage on all middleware in order.\n */\n async runOnUsage(\n ctx: ChatMiddlewareContext,\n usage: UsageInfo,\n ): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onUsage) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onUsage(ctx, usage)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onUsage',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onFinish on all middleware in order.\n */\n async runOnFinish(\n ctx: ChatMiddlewareContext,\n info: FinishInfo,\n ): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onFinish) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onFinish(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onFinish',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onAbort on all middleware in order.\n */\n async runOnAbort(ctx: ChatMiddlewareContext, info: AbortInfo): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onAbort) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onAbort(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onAbort',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onError on all middleware in order.\n */\n async runOnError(ctx: ChatMiddlewareContext, info: ErrorInfo): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onError) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onError(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onError',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onIteration on all middleware in order.\n * Called at the start of each agent loop iteration.\n */\n async runOnIteration(\n ctx: ChatMiddlewareContext,\n info: IterationInfo,\n ): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onIteration) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onIteration(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onIteration',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n\n /**\n * Run onToolPhaseComplete on all middleware in order.\n * Called after all tool calls in an iteration have been processed.\n */\n async runOnToolPhaseComplete(\n ctx: ChatMiddlewareContext,\n info: ToolPhaseCompleteInfo,\n ): Promise<void> {\n for (const mw of this.middlewares) {\n if (mw.onToolPhaseComplete) {\n const skip = shouldSkipInstrumentation(mw)\n const start = Date.now()\n await mw.onToolPhaseComplete(ctx, info)\n if (!skip) {\n aiEventClient.emit('middleware:hook:executed', {\n ...instrumentCtx(ctx),\n middlewareName: mw.name || 'unnamed',\n hookName: 'onToolPhaseComplete',\n iteration: ctx.iteration,\n duration: Date.now() - start,\n hasTransform: false,\n })\n }\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export type { ChatMiddleware, ChatMiddlewareContext, ChatMiddlewarePhase, ChatMiddlewareConfig, ToolCallHookContext, BeforeToolCallDecision, AfterToolCallInfo, IterationInfo, ToolPhaseCompleteInfo, UsageInfo, FinishInfo, AbortInfo, ErrorInfo, } from './types.js';
2
+ export { MiddlewareRunner } from './compose.js';
@@ -0,0 +1,89 @@
1
+ import { ChatMiddleware } from './types.js';
2
+ /**
3
+ * A cache entry stored by the tool cache middleware.
4
+ */
5
+ export interface ToolCacheEntry {
6
+ result: unknown;
7
+ timestamp: number;
8
+ }
9
+ /**
10
+ * Custom storage backend for the tool cache middleware.
11
+ *
12
+ * When provided, the middleware delegates all cache operations to this storage
13
+ * instead of using the built-in in-memory Map. This enables external storage
14
+ * backends like Redis, localStorage, databases, etc.
15
+ *
16
+ * All methods may return a Promise for async storage backends.
17
+ */
18
+ export interface ToolCacheStorage {
19
+ getItem: (key: string) => ToolCacheEntry | undefined | Promise<ToolCacheEntry | undefined>;
20
+ setItem: (key: string, value: ToolCacheEntry) => void | Promise<void>;
21
+ deleteItem: (key: string) => void | Promise<void>;
22
+ }
23
+ /**
24
+ * Options for the tool cache middleware.
25
+ */
26
+ export interface ToolCacheMiddlewareOptions {
27
+ /**
28
+ * Maximum number of entries in the cache.
29
+ * When exceeded, the oldest entry is evicted (LRU).
30
+ *
31
+ * Only applies to the default in-memory storage.
32
+ * When a custom `storage` is provided, capacity management is the storage's responsibility.
33
+ *
34
+ * @default 100
35
+ */
36
+ maxSize?: number;
37
+ /**
38
+ * Time-to-live in milliseconds. Entries older than this are not served from cache.
39
+ * @default Infinity (no expiry)
40
+ */
41
+ ttl?: number;
42
+ /**
43
+ * Tool names to cache. If not provided, all tools are cached.
44
+ */
45
+ toolNames?: Array<string>;
46
+ /**
47
+ * Custom function to generate a cache key from tool name and args.
48
+ * Defaults to `JSON.stringify([toolName, args])`.
49
+ */
50
+ keyFn?: (toolName: string, args: unknown) => string;
51
+ /**
52
+ * Custom storage backend. When provided, the middleware uses this instead of
53
+ * the built-in in-memory Map. The storage is responsible for its own capacity
54
+ * management — the `maxSize` option is ignored.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * toolCacheMiddleware({
59
+ * storage: {
60
+ * getItem: (key) => redisClient.get(key).then(v => v ? JSON.parse(v) : undefined),
61
+ * setItem: (key, value) => redisClient.set(key, JSON.stringify(value)),
62
+ * deleteItem: (key) => redisClient.del(key),
63
+ * },
64
+ * })
65
+ * ```
66
+ */
67
+ storage?: ToolCacheStorage;
68
+ }
69
+ /**
70
+ * Creates a middleware that caches tool call results based on tool name + arguments.
71
+ *
72
+ * When a tool is called with the same name and arguments as a previous call,
73
+ * the cached result is returned immediately without executing the tool.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { chat, toolCacheMiddleware } from '@tanstack/ai'
78
+ *
79
+ * const stream = chat({
80
+ * adapter,
81
+ * messages,
82
+ * tools: [weatherTool, stockTool],
83
+ * middleware: [
84
+ * toolCacheMiddleware({ ttl: 60_000, toolNames: ['getWeather'] }),
85
+ * ],
86
+ * })
87
+ * ```
88
+ */
89
+ export declare function toolCacheMiddleware(options?: ToolCacheMiddlewareOptions): ChatMiddleware;
@@ -0,0 +1,76 @@
1
+ function defaultKeyFn(toolName, args) {
2
+ return JSON.stringify([toolName, args]);
3
+ }
4
+ function createDefaultStorage(maxSize) {
5
+ const cache = /* @__PURE__ */ new Map();
6
+ return {
7
+ getItem: (key) => {
8
+ const entry = cache.get(key);
9
+ if (entry !== void 0) {
10
+ cache.delete(key);
11
+ cache.set(key, entry);
12
+ }
13
+ return entry;
14
+ },
15
+ setItem: (key, value) => {
16
+ if (cache.has(key)) {
17
+ cache.delete(key);
18
+ } else if (cache.size >= maxSize) {
19
+ const firstKey = cache.keys().next().value;
20
+ if (firstKey !== void 0) {
21
+ cache.delete(firstKey);
22
+ }
23
+ }
24
+ cache.set(key, value);
25
+ },
26
+ deleteItem: (key) => {
27
+ cache.delete(key);
28
+ }
29
+ };
30
+ }
31
+ function toolCacheMiddleware(options = {}) {
32
+ const {
33
+ maxSize = 100,
34
+ ttl = Infinity,
35
+ toolNames,
36
+ keyFn = defaultKeyFn,
37
+ storage = createDefaultStorage(maxSize)
38
+ } = options;
39
+ return {
40
+ name: "tool-cache-middleware",
41
+ onBeforeToolCall: async (_ctx, hookCtx) => {
42
+ if (toolNames && !toolNames.includes(hookCtx.toolName)) {
43
+ return void 0;
44
+ }
45
+ const key = keyFn(hookCtx.toolName, hookCtx.args);
46
+ const entry = await storage.getItem(key);
47
+ if (entry) {
48
+ const age = Date.now() - entry.timestamp;
49
+ if (age < ttl) {
50
+ return { type: "skip", result: entry.result };
51
+ }
52
+ await storage.deleteItem(key);
53
+ }
54
+ return void 0;
55
+ },
56
+ onAfterToolCall: async (_ctx, info) => {
57
+ if (!info.ok) return;
58
+ if (toolNames && !toolNames.includes(info.toolName)) return;
59
+ let parsedArgs;
60
+ try {
61
+ parsedArgs = JSON.parse(info.toolCall.function.arguments.trim() || "{}");
62
+ } catch {
63
+ return;
64
+ }
65
+ const key = keyFn(info.toolName, parsedArgs);
66
+ await storage.setItem(key, {
67
+ result: info.result,
68
+ timestamp: Date.now()
69
+ });
70
+ }
71
+ };
72
+ }
73
+ export {
74
+ toolCacheMiddleware
75
+ };
76
+ //# sourceMappingURL=tool-cache-middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-cache-middleware.js","sources":["../../../../../src/activities/chat/middleware/tool-cache-middleware.ts"],"sourcesContent":["import type { ChatMiddleware } from './types'\n\n/**\n * A cache entry stored by the tool cache middleware.\n */\nexport interface ToolCacheEntry {\n result: unknown\n timestamp: number\n}\n\n/**\n * Custom storage backend for the tool cache middleware.\n *\n * When provided, the middleware delegates all cache operations to this storage\n * instead of using the built-in in-memory Map. This enables external storage\n * backends like Redis, localStorage, databases, etc.\n *\n * All methods may return a Promise for async storage backends.\n */\nexport interface ToolCacheStorage {\n getItem: (\n key: string,\n ) => ToolCacheEntry | undefined | Promise<ToolCacheEntry | undefined>\n setItem: (key: string, value: ToolCacheEntry) => void | Promise<void>\n deleteItem: (key: string) => void | Promise<void>\n}\n\n/**\n * Options for the tool cache middleware.\n */\nexport interface ToolCacheMiddlewareOptions {\n /**\n * Maximum number of entries in the cache.\n * When exceeded, the oldest entry is evicted (LRU).\n *\n * Only applies to the default in-memory storage.\n * When a custom `storage` is provided, capacity management is the storage's responsibility.\n *\n * @default 100\n */\n maxSize?: number\n\n /**\n * Time-to-live in milliseconds. Entries older than this are not served from cache.\n * @default Infinity (no expiry)\n */\n ttl?: number\n\n /**\n * Tool names to cache. If not provided, all tools are cached.\n */\n toolNames?: Array<string>\n\n /**\n * Custom function to generate a cache key from tool name and args.\n * Defaults to `JSON.stringify([toolName, args])`.\n */\n keyFn?: (toolName: string, args: unknown) => string\n\n /**\n * Custom storage backend. When provided, the middleware uses this instead of\n * the built-in in-memory Map. The storage is responsible for its own capacity\n * management — the `maxSize` option is ignored.\n *\n * @example\n * ```ts\n * toolCacheMiddleware({\n * storage: {\n * getItem: (key) => redisClient.get(key).then(v => v ? JSON.parse(v) : undefined),\n * setItem: (key, value) => redisClient.set(key, JSON.stringify(value)),\n * deleteItem: (key) => redisClient.del(key),\n * },\n * })\n * ```\n */\n storage?: ToolCacheStorage\n}\n\nfunction defaultKeyFn(toolName: string, args: unknown): string {\n return JSON.stringify([toolName, args])\n}\n\nfunction createDefaultStorage(maxSize: number): ToolCacheStorage {\n const cache = new Map<string, ToolCacheEntry>()\n\n return {\n getItem: (key) => {\n const entry = cache.get(key)\n if (entry !== undefined) {\n // Refresh recency: delete and re-insert so this key becomes newest\n cache.delete(key)\n cache.set(key, entry)\n }\n return entry\n },\n setItem: (key, value) => {\n // Delete first so re-inserts also refresh recency\n if (cache.has(key)) {\n cache.delete(key)\n } else if (cache.size >= maxSize) {\n // LRU eviction: Map iteration order is insertion order — first key is least recently used\n const firstKey = cache.keys().next().value\n if (firstKey !== undefined) {\n cache.delete(firstKey)\n }\n }\n cache.set(key, value)\n },\n deleteItem: (key) => {\n cache.delete(key)\n },\n }\n}\n\n/**\n * Creates a middleware that caches tool call results based on tool name + arguments.\n *\n * When a tool is called with the same name and arguments as a previous call,\n * the cached result is returned immediately without executing the tool.\n *\n * @example\n * ```ts\n * import { chat, toolCacheMiddleware } from '@tanstack/ai'\n *\n * const stream = chat({\n * adapter,\n * messages,\n * tools: [weatherTool, stockTool],\n * middleware: [\n * toolCacheMiddleware({ ttl: 60_000, toolNames: ['getWeather'] }),\n * ],\n * })\n * ```\n */\nexport function toolCacheMiddleware(\n options: ToolCacheMiddlewareOptions = {},\n): ChatMiddleware {\n const {\n maxSize = 100,\n ttl = Infinity,\n toolNames,\n keyFn = defaultKeyFn,\n storage = createDefaultStorage(maxSize),\n } = options\n\n return {\n name: 'tool-cache-middleware',\n\n onBeforeToolCall: async (_ctx, hookCtx) => {\n if (toolNames && !toolNames.includes(hookCtx.toolName)) {\n return undefined\n }\n\n const key = keyFn(hookCtx.toolName, hookCtx.args)\n const entry = await storage.getItem(key)\n\n if (entry) {\n const age = Date.now() - entry.timestamp\n if (age < ttl) {\n return { type: 'skip', result: entry.result }\n }\n // Expired — remove\n await storage.deleteItem(key)\n }\n\n return undefined\n },\n\n onAfterToolCall: async (_ctx, info) => {\n if (!info.ok) return\n if (toolNames && !toolNames.includes(info.toolName)) return\n\n // Re-derive the key from the raw arguments to match what onBeforeToolCall produces\n let parsedArgs: unknown\n try {\n parsedArgs = JSON.parse(info.toolCall.function.arguments.trim() || '{}')\n } catch {\n return\n }\n\n const key = keyFn(info.toolName, parsedArgs)\n\n await storage.setItem(key, {\n result: info.result,\n timestamp: Date.now(),\n })\n },\n }\n}\n"],"names":[],"mappings":"AA8EA,SAAS,aAAa,UAAkB,MAAuB;AAC7D,SAAO,KAAK,UAAU,CAAC,UAAU,IAAI,CAAC;AACxC;AAEA,SAAS,qBAAqB,SAAmC;AAC/D,QAAM,4BAAY,IAAA;AAElB,SAAO;AAAA,IACL,SAAS,CAAC,QAAQ;AAChB,YAAM,QAAQ,MAAM,IAAI,GAAG;AAC3B,UAAI,UAAU,QAAW;AAEvB,cAAM,OAAO,GAAG;AAChB,cAAM,IAAI,KAAK,KAAK;AAAA,MACtB;AACA,aAAO;AAAA,IACT;AAAA,IACA,SAAS,CAAC,KAAK,UAAU;AAEvB,UAAI,MAAM,IAAI,GAAG,GAAG;AAClB,cAAM,OAAO,GAAG;AAAA,MAClB,WAAW,MAAM,QAAQ,SAAS;AAEhC,cAAM,WAAW,MAAM,KAAA,EAAO,OAAO;AACrC,YAAI,aAAa,QAAW;AAC1B,gBAAM,OAAO,QAAQ;AAAA,QACvB;AAAA,MACF;AACA,YAAM,IAAI,KAAK,KAAK;AAAA,IACtB;AAAA,IACA,YAAY,CAAC,QAAQ;AACnB,YAAM,OAAO,GAAG;AAAA,IAClB;AAAA,EAAA;AAEJ;AAsBO,SAAS,oBACd,UAAsC,IACtB;AAChB,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,qBAAqB,OAAO;AAAA,EAAA,IACpC;AAEJ,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,kBAAkB,OAAO,MAAM,YAAY;AACzC,UAAI,aAAa,CAAC,UAAU,SAAS,QAAQ,QAAQ,GAAG;AACtD,eAAO;AAAA,MACT;AAEA,YAAM,MAAM,MAAM,QAAQ,UAAU,QAAQ,IAAI;AAChD,YAAM,QAAQ,MAAM,QAAQ,QAAQ,GAAG;AAEvC,UAAI,OAAO;AACT,cAAM,MAAM,KAAK,IAAA,IAAQ,MAAM;AAC/B,YAAI,MAAM,KAAK;AACb,iBAAO,EAAE,MAAM,QAAQ,QAAQ,MAAM,OAAA;AAAA,QACvC;AAEA,cAAM,QAAQ,WAAW,GAAG;AAAA,MAC9B;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,iBAAiB,OAAO,MAAM,SAAS;AACrC,UAAI,CAAC,KAAK,GAAI;AACd,UAAI,aAAa,CAAC,UAAU,SAAS,KAAK,QAAQ,EAAG;AAGrD,UAAI;AACJ,UAAI;AACF,qBAAa,KAAK,MAAM,KAAK,SAAS,SAAS,UAAU,KAAA,KAAU,IAAI;AAAA,MACzE,QAAQ;AACN;AAAA,MACF;AAEA,YAAM,MAAM,MAAM,KAAK,UAAU,UAAU;AAE3C,YAAM,QAAQ,QAAQ,KAAK;AAAA,QACzB,QAAQ,KAAK;AAAA,QACb,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AAAA,IACH;AAAA,EAAA;AAEJ;"}