@tyvm/knowhow 0.0.145 → 0.0.146

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 (139) hide show
  1. package/package.json +1 -1
  2. package/src/chat/CliChatService.ts +16 -0
  3. package/src/chat/modules/AgentModule.ts +30 -23
  4. package/src/chat/modules/SessionsModule.ts +7 -5
  5. package/src/chat/types.ts +3 -2
  6. package/src/clients/anthropic.ts +24 -0
  7. package/src/clients/fireworks.ts +3 -0
  8. package/src/clients/gemini.ts +8 -0
  9. package/src/clients/openai.ts +7 -4
  10. package/src/clients/types.ts +30 -7
  11. package/src/clients/xai.ts +23 -4
  12. package/src/commands/agent.ts +10 -1
  13. package/src/commands/services.ts +49 -3
  14. package/src/plugins/plugins.ts +16 -0
  15. package/src/processors/TokenCompressor.ts +40 -8
  16. package/src/processors/ToolResponseCache.ts +17 -8
  17. package/src/processors/tools/boundedToolResponse.ts +49 -0
  18. package/src/processors/tools/grepToolResponse.ts +164 -38
  19. package/src/processors/tools/index.ts +2 -0
  20. package/src/processors/tools/jqToolResponse.ts +40 -10
  21. package/src/processors/tools/listStoredToolResponses.ts +51 -10
  22. package/src/processors/tools/tailToolResponse.ts +85 -17
  23. package/src/services/LazyToolsService.ts +7 -0
  24. package/src/services/Mcp.ts +9 -2
  25. package/src/services/RuntimeReloadService.ts +31 -0
  26. package/src/services/Tools.ts +11 -0
  27. package/src/services/TracingService.ts +116 -0
  28. package/src/services/index.ts +8 -0
  29. package/src/services/modules/index.ts +45 -2
  30. package/src/services/modules/types.ts +8 -0
  31. package/src/util/Trace.ts +236 -0
  32. package/tests/clients/fireworks.test.ts +65 -0
  33. package/tests/clients/reasoningEffort.test.ts +79 -0
  34. package/tests/compressor/toolResponseCache.test.ts +2 -1
  35. package/tests/manual/clients/completions.test.ts +27 -10
  36. package/tests/processors/TokenCompressor.test.ts +17 -3
  37. package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
  38. package/tests/processors/tools/grepToolResponse.test.ts +88 -1
  39. package/tests/unit/plugins/pluginLoading.test.ts +31 -0
  40. package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
  41. package/ts_build/package.json +1 -1
  42. package/ts_build/src/chat/CliChatService.js +15 -0
  43. package/ts_build/src/chat/CliChatService.js.map +1 -1
  44. package/ts_build/src/chat/modules/AgentModule.js +13 -19
  45. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  46. package/ts_build/src/chat/modules/SessionsModule.js +1 -6
  47. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  48. package/ts_build/src/chat/types.d.ts +3 -2
  49. package/ts_build/src/clients/anthropic.d.ts +1 -0
  50. package/ts_build/src/clients/anthropic.js +12 -0
  51. package/ts_build/src/clients/anthropic.js.map +1 -1
  52. package/ts_build/src/clients/fireworks.js +1 -0
  53. package/ts_build/src/clients/fireworks.js.map +1 -1
  54. package/ts_build/src/clients/gemini.js +8 -0
  55. package/ts_build/src/clients/gemini.js.map +1 -1
  56. package/ts_build/src/clients/openai.js +4 -2
  57. package/ts_build/src/clients/openai.js.map +1 -1
  58. package/ts_build/src/clients/types.d.ts +4 -1
  59. package/ts_build/src/clients/types.js +14 -0
  60. package/ts_build/src/clients/types.js.map +1 -1
  61. package/ts_build/src/clients/xai.d.ts +1 -0
  62. package/ts_build/src/clients/xai.js +23 -4
  63. package/ts_build/src/clients/xai.js.map +1 -1
  64. package/ts_build/src/commands/agent.js +6 -1
  65. package/ts_build/src/commands/agent.js.map +1 -1
  66. package/ts_build/src/commands/services.d.ts +2 -0
  67. package/ts_build/src/commands/services.js +45 -3
  68. package/ts_build/src/commands/services.js.map +1 -1
  69. package/ts_build/src/plugins/plugins.d.ts +1 -0
  70. package/ts_build/src/plugins/plugins.js +14 -0
  71. package/ts_build/src/plugins/plugins.js.map +1 -1
  72. package/ts_build/src/processors/TokenCompressor.js +15 -6
  73. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  74. package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
  75. package/ts_build/src/processors/ToolResponseCache.js +8 -8
  76. package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
  77. package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
  78. package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
  79. package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
  80. package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
  81. package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
  82. package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
  83. package/ts_build/src/processors/tools/index.d.ts +2 -2
  84. package/ts_build/src/processors/tools/index.js.map +1 -1
  85. package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
  86. package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
  87. package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
  88. package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
  89. package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
  90. package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
  91. package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
  92. package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
  93. package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
  94. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  95. package/ts_build/src/services/LazyToolsService.js +5 -0
  96. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  97. package/ts_build/src/services/Mcp.js +8 -2
  98. package/ts_build/src/services/Mcp.js.map +1 -1
  99. package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
  100. package/ts_build/src/services/RuntimeReloadService.js +26 -0
  101. package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
  102. package/ts_build/src/services/Tools.d.ts +1 -0
  103. package/ts_build/src/services/Tools.js +5 -0
  104. package/ts_build/src/services/Tools.js.map +1 -1
  105. package/ts_build/src/services/TracingService.d.ts +16 -0
  106. package/ts_build/src/services/TracingService.js +35 -0
  107. package/ts_build/src/services/TracingService.js.map +1 -0
  108. package/ts_build/src/services/index.d.ts +6 -0
  109. package/ts_build/src/services/index.js +7 -1
  110. package/ts_build/src/services/index.js.map +1 -1
  111. package/ts_build/src/services/modules/index.d.ts +5 -0
  112. package/ts_build/src/services/modules/index.js +28 -2
  113. package/ts_build/src/services/modules/index.js.map +1 -1
  114. package/ts_build/src/services/modules/types.d.ts +3 -0
  115. package/ts_build/src/util/Trace.d.ts +13 -0
  116. package/ts_build/src/util/Trace.js +107 -0
  117. package/ts_build/src/util/Trace.js.map +1 -0
  118. package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
  119. package/ts_build/tests/clients/fireworks.test.js +65 -0
  120. package/ts_build/tests/clients/fireworks.test.js.map +1 -0
  121. package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
  122. package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
  123. package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
  124. package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
  125. package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
  126. package/ts_build/tests/manual/clients/completions.test.js +19 -6
  127. package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
  128. package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
  129. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  130. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
  131. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
  132. package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
  133. package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
  134. package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
  135. package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
  136. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  137. package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
  138. package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
  139. package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Trace.ts — Method / class decorators and `tracify()` helper for the
3
+ * @tyvm/knowhow package.
4
+ *
5
+ * These decorators are zero-cost when no tracer is registered (the default).
6
+ * Once `@tyvm/knowhow-module-tracing` (or any other module) calls
7
+ * `TracingService.register(impl)`, every decorated method starts emitting
8
+ * real spans automatically — no code changes required.
9
+ *
10
+ * API mirrors the backend's `Trace.ts` so patterns learned there carry over:
11
+ *
12
+ * @Trace() — wrap a single method
13
+ * @TraceAll() — wrap every public method on a class
14
+ * tracify(obj, ns) — wrap an already-constructed instance
15
+ *
16
+ * @example
17
+ * import { Trace, TraceAll, tracify } from '../util/Trace';
18
+ *
19
+ * @TraceAll()
20
+ * export class MessageProcessor {
21
+ * async process(msg: Message) { ... } // → span "MessageProcessor.process"
22
+ * }
23
+ *
24
+ * @Trace()
25
+ * async fetchEmbeddings(text: string) { ... } // → span "ClassName.fetchEmbeddings"
26
+ *
27
+ * export const myClient = tracify(new SomeClient(), 'someClient');
28
+ */
29
+
30
+ import { TracingService } from "../services/TracingService";
31
+
32
+ // ─── Shared span lifecycle helper ─────────────────────────────────────────────
33
+
34
+ function runWithSpan<T>(
35
+ spanName: string,
36
+ attributes: Record<string, string | number | boolean>,
37
+ fn: () => T
38
+ ): T {
39
+ const span = TracingService.startSpan(spanName, attributes);
40
+
41
+ const finish = (err?: unknown) => {
42
+ if (err !== undefined) {
43
+ span.recordError(err);
44
+ }
45
+ span.end();
46
+ };
47
+
48
+ try {
49
+ const result = TracingService.withSpan(span, fn);
50
+
51
+ if (result instanceof Promise) {
52
+ return result.then(
53
+ (value) => {
54
+ finish();
55
+ return value;
56
+ },
57
+ (err) => {
58
+ finish(err);
59
+ throw err;
60
+ }
61
+ ) as unknown as T;
62
+ }
63
+
64
+ finish();
65
+ return result;
66
+ } catch (err) {
67
+ finish(err);
68
+ throw err;
69
+ }
70
+ }
71
+
72
+ // ─── @Trace() — single-method decorator ──────────────────────────────────────
73
+
74
+ export interface TraceOptions {
75
+ /** Override the span name (default: `ClassName.methodName`) */
76
+ spanName?: string;
77
+ /** Additional static attributes attached to every span */
78
+ attributes?: Record<string, string | number | boolean>;
79
+ }
80
+
81
+ /**
82
+ * @Trace() method decorator — wraps the decorated method in a tracing span.
83
+ *
84
+ * Span name defaults to `ClassName.methodName`. Errors are recorded on the
85
+ * span and re-thrown. Works with both sync and async methods.
86
+ *
87
+ * @example
88
+ * @Trace()
89
+ * async fetchEmbeddings(text: string) { ... }
90
+ *
91
+ * @Trace({ spanName: 'embeddings.fetch', attributes: { 'peer.service': 'openai' } })
92
+ * async fetchEmbeddings(text: string) { ... }
93
+ */
94
+ export function Trace(options: TraceOptions = {}) {
95
+ return function (
96
+ target: object,
97
+ propertyKey: string,
98
+ descriptor: PropertyDescriptor
99
+ ): PropertyDescriptor {
100
+ const originalMethod = descriptor.value as (...args: unknown[]) => unknown;
101
+ const className = (target as any).constructor?.name ?? "Unknown";
102
+ const resolvedSpanName =
103
+ options.spanName ?? `${className}.${propertyKey}`;
104
+ const staticAttrs: Record<string, string | number | boolean> = {
105
+ "code.function": propertyKey,
106
+ "code.namespace": className,
107
+ ...options.attributes,
108
+ };
109
+
110
+ descriptor.value = function (this: unknown, ...args: unknown[]) {
111
+ return runWithSpan(resolvedSpanName, staticAttrs, () =>
112
+ originalMethod.apply(this, args)
113
+ );
114
+ };
115
+
116
+ return descriptor;
117
+ };
118
+ }
119
+
120
+ // ─── @TraceAll() — class decorator ───────────────────────────────────────────
121
+
122
+ export interface TraceAllOptions {
123
+ /** Additional static attributes attached to every span */
124
+ attributes?: Record<string, string | number | boolean>;
125
+ /** Method names to skip (in addition to constructor and `_`-prefixed) */
126
+ skip?: string[];
127
+ }
128
+
129
+ /**
130
+ * @TraceAll() class decorator — wraps every public method on the class in a
131
+ * tracing span named `ClassName.methodName`.
132
+ *
133
+ * Skips the constructor and any methods whose name starts with `_`.
134
+ *
135
+ * @example
136
+ * @TraceAll()
137
+ * export class MessageProcessor {
138
+ * async process(msg: Message) { ... } // → span "MessageProcessor.process"
139
+ * }
140
+ */
141
+ export function TraceAll(options: TraceAllOptions = {}) {
142
+ const skip = new Set(options.skip ?? []);
143
+
144
+ return function <T extends { new (...args: unknown[]): object }>(
145
+ constructor: T
146
+ ): T {
147
+ const className = constructor.name;
148
+ const proto = constructor.prototype as Record<string, unknown>;
149
+
150
+ for (const key of Object.getOwnPropertyNames(proto)) {
151
+ if (key === "constructor") continue;
152
+ if (key.startsWith("_")) continue;
153
+ if (skip.has(key)) continue;
154
+
155
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
156
+ if (!descriptor || typeof descriptor.value !== "function") continue;
157
+
158
+ const originalMethod = descriptor.value as (
159
+ ...args: unknown[]
160
+ ) => unknown;
161
+ const spanName = `${className}.${key}`;
162
+ const staticAttrs: Record<string, string | number | boolean> = {
163
+ "code.function": key,
164
+ "code.namespace": className,
165
+ ...options.attributes,
166
+ };
167
+
168
+ descriptor.value = function (this: unknown, ...args: unknown[]) {
169
+ return runWithSpan(spanName, staticAttrs, () =>
170
+ originalMethod.apply(this, args)
171
+ );
172
+ };
173
+
174
+ Object.defineProperty(proto, key, descriptor);
175
+ }
176
+
177
+ return constructor;
178
+ };
179
+ }
180
+
181
+ // ─── tracify() — instance-level wrapper ──────────────────────────────────────
182
+
183
+ /**
184
+ * tracify — wraps every method on an already-constructed object instance in a
185
+ * tracing span.
186
+ *
187
+ * Ideal for third-party singletons (SDK clients etc.) that cannot be decorated
188
+ * at the class level.
189
+ *
190
+ * @param instance The object whose methods should be traced.
191
+ * @param namespace Prefix for span names, e.g. "embeddingsClient".
192
+ * @param options Optional extra attributes / skip list.
193
+ *
194
+ * @example
195
+ * export const embeddingsClient = tracify(new OpenAI(), 'openai');
196
+ */
197
+ export function tracify<T extends object>(
198
+ instance: T,
199
+ namespace: string,
200
+ options: TraceAllOptions = {}
201
+ ): T {
202
+ const skip = new Set(options.skip ?? []);
203
+
204
+ return new Proxy(instance, {
205
+ get(target, prop, receiver) {
206
+ const value = Reflect.get(target, prop, receiver);
207
+ const methodName = typeof prop === "string" ? prop : String(prop);
208
+
209
+ // Pass symbols through unwrapped
210
+ if (typeof prop === "symbol") return value;
211
+
212
+ // Skip private / user-skipped names
213
+ if (methodName.startsWith("_") || skip.has(methodName)) return value;
214
+
215
+ // Recursively wrap nested objects (e.g. prisma.user.findMany)
216
+ if (value !== null && typeof value === "object") {
217
+ return tracify(value as object, `${namespace}.${methodName}`, options);
218
+ }
219
+
220
+ if (typeof value !== "function") return value;
221
+
222
+ const spanName = `${namespace}.${methodName}`;
223
+ const staticAttrs: Record<string, string | number | boolean> = {
224
+ "code.function": methodName,
225
+ "code.namespace": namespace,
226
+ ...options.attributes,
227
+ };
228
+
229
+ return function (this: unknown, ...args: unknown[]) {
230
+ return runWithSpan(spanName, staticAttrs, () =>
231
+ (value as Function).apply(target, args)
232
+ );
233
+ };
234
+ },
235
+ });
236
+ }
@@ -0,0 +1,65 @@
1
+ import http from "../../src/utils/http";
2
+ import { GenericFireworksClient } from "../../src/clients/fireworks";
3
+ import { FireworksModels } from "../../src/clients/pricing/fireworks";
4
+
5
+ jest.mock("../../src/utils/http", () => ({
6
+ __esModule: true,
7
+ default: { post: jest.fn() },
8
+ }));
9
+
10
+ const post = http.post as jest.MockedFunction<typeof http.post>;
11
+
12
+ describe("GenericFireworksClient", () => {
13
+ beforeEach(() => {
14
+ post.mockReset();
15
+ post.mockResolvedValue({
16
+ data: {
17
+ choices: [{ message: { role: "assistant", content: "ok" } }],
18
+ model: FireworksModels.DeepseekV4Flash,
19
+ usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
20
+ },
21
+ } as any);
22
+ });
23
+
24
+ it("removes non-Fireworks options and tool return metadata", async () => {
25
+ const client = new GenericFireworksClient("test-key");
26
+
27
+ await client.createChatCompletion({
28
+ model: FireworksModels.DeepseekV4Flash,
29
+ messages: [{ role: "user", content: "hello" }],
30
+ max_tokens: 100,
31
+ reasoning_summary: true,
32
+ long_ttl_cache: true,
33
+ tools: [{
34
+ type: "function",
35
+ function: {
36
+ name: "lookup",
37
+ description: "Look something up",
38
+ parameters: { type: "object", properties: {} },
39
+ returns: { type: "string" },
40
+ } as any,
41
+ }],
42
+ });
43
+
44
+ expect(post).toHaveBeenCalledTimes(1);
45
+ const [url, body, requestOptions] = post.mock.calls[0];
46
+ expect(url).toBe("https://api.fireworks.ai/inference/v1/chat/completions");
47
+ expect(requestOptions).toMatchObject({
48
+ headers: { Authorization: "Bearer test-key" },
49
+ });
50
+ expect(body).toMatchObject({
51
+ model: FireworksModels.DeepseekV4Flash,
52
+ messages: [{ role: "user", content: "hello" }],
53
+ max_tokens: 100,
54
+ tool_choice: "auto",
55
+ });
56
+ const serializedBody = JSON.parse(JSON.stringify(body));
57
+ expect(serializedBody).not.toHaveProperty("reasoning_summary");
58
+ expect(serializedBody).not.toHaveProperty("long_ttl_cache");
59
+ expect(serializedBody.tools[0].function).toEqual({
60
+ name: "lookup",
61
+ description: "Look something up",
62
+ parameters: { type: "object", properties: {} },
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,79 @@
1
+ import { GenericAnthropicClient } from "../../src/clients/anthropic";
2
+ import { GenericGeminiClient } from "../../src/clients/gemini";
3
+ import { GenericOpenAiClient } from "../../src/clients/openai";
4
+ import { AnthropicModels } from "../../src/clients/pricing";
5
+ import {
6
+ REASONING_EFFORTS,
7
+ ReasoningEffort,
8
+ isReasoningEffort,
9
+ } from "../../src/clients/types";
10
+ import {
11
+ GoogleThinkingBudgetModels,
12
+ GoogleThinkingLevelModels,
13
+ } from "../../src/types";
14
+ import { GenericXAIClient } from "../../src/clients/xai";
15
+
16
+ const options = (model: string, reasoning_effort: ReasoningEffort) => ({
17
+ model,
18
+ reasoning_effort,
19
+ messages: [],
20
+ });
21
+
22
+ describe("reasoning effort values", () => {
23
+ it("accepts the complete provider-neutral set and rejects other values", () => {
24
+ expect(REASONING_EFFORTS).toEqual([
25
+ "none",
26
+ "minimal",
27
+ "low",
28
+ "medium",
29
+ "high",
30
+ "xhigh",
31
+ "max",
32
+ ]);
33
+ REASONING_EFFORTS.forEach((effort) => expect(isReasoningEffort(effort)).toBe(true));
34
+ expect(isReasoningEffort("extra-high")).toBe(false);
35
+ expect(isReasoningEffort(undefined)).toBe(false);
36
+ });
37
+
38
+ it("maps provider-neutral values to OpenAI legacy and Responses spellings", () => {
39
+ const client = new GenericOpenAiClient("fake-key");
40
+ expect(client.resolveReasoningEffort(options("unknown", "none"))).toBe("low");
41
+ expect(client.resolveReasoningEffort(options("unknown", "minimal"))).toBe("low");
42
+ expect(client.resolveReasoningEffort(options("unknown", "xhigh"))).toBe("high");
43
+ expect(client.resolveReasoningEffort(options("unknown", "max"))).toBe("high");
44
+ expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
45
+ });
46
+
47
+ it("maps all values to valid Gemini thinking levels and budgets", () => {
48
+ const client = new GenericGeminiClient("fake-key");
49
+ const levelModel = GoogleThinkingLevelModels[0];
50
+ const budgetModel = GoogleThinkingBudgetModels[0];
51
+
52
+ expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
53
+ expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
54
+ expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
55
+ expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
56
+ expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
57
+ expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
58
+ expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
59
+ expect(client.buildThinkingConfig(options(budgetModel, "max"))).toEqual({ thinkingBudget: -1 });
60
+ });
61
+
62
+ it("maps provider-neutral values to Anthropic output effort", () => {
63
+ const client = new GenericAnthropicClient("fake-key");
64
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "none")).toBeUndefined();
65
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "minimal")).toEqual({ effort: "low" });
66
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "xhigh")).toEqual({ effort: "xhigh" });
67
+ expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "max")).toEqual({ effort: "max" });
68
+ expect(client.getEffortOutputConfig("unsupported", "high")).toBeUndefined();
69
+ });
70
+
71
+ it("maps provider-neutral values to xAI legacy effort", () => {
72
+ const client = new GenericXAIClient("fake-key");
73
+ expect(client.resolveLegacyReasoningEffort("none")).toBeUndefined();
74
+ expect(client.resolveLegacyReasoningEffort("minimal")).toBe("low");
75
+ expect(client.resolveLegacyReasoningEffort("medium")).toBe("medium");
76
+ expect(client.resolveLegacyReasoningEffort("xhigh")).toBe("high");
77
+ expect(client.resolveLegacyReasoningEffort("max")).toBe("high");
78
+ });
79
+ });
@@ -111,7 +111,8 @@ describe("ToolResponseCache - MCP Format Support", () => {
111
111
  it("should filter data array with ._data | map(select(.private == true))", async () => {
112
112
  const result = await cache.queryToolResponse(
113
113
  "github-repos",
114
- "._data | map(select(.private == true))"
114
+ "._data | map(select(.private == true))",
115
+ { maxCharacters: 50_000 }
115
116
  );
116
117
 
117
118
  // Parse and verify filtering worked
@@ -17,6 +17,7 @@ import { OpenAiChatModels, OpenAiReasoningModels, OpenAiResponsesOnlyModels } fr
17
17
  import { AnthropicTextModels, AnthropicLimitedAvailabilityModels } from "../../../src/clients/pricing/anthropic";
18
18
  import { GoogleTextModels } from "../../../src/clients/pricing/google";
19
19
  import { XaiTextModels, XaiDeprecatedTextModels } from "../../../src/clients/pricing/xai";
20
+ import { FireworksTextPricing } from "../../../src/clients/pricing/fireworks";
20
21
 
21
22
  const OUTPUT_FILE = path.join(__dirname, "completions.json");
22
23
  const PROMPT = "Say hello in 5 words.";
@@ -29,6 +30,7 @@ interface ModelEntry {
29
30
  provider: string;
30
31
  model: string;
31
32
  envKey: string;
33
+ reasoningEffort?: "low";
32
34
  }
33
35
 
34
36
  function openaiEntries(models: string[]): ModelEntry[] {
@@ -47,19 +49,35 @@ function xaiEntries(models: string[]): ModelEntry[] {
47
49
  return models.map((model) => ({ provider: "xai", model, envKey: "XAI_API_KEY" }));
48
50
  }
49
51
 
52
+ function fireworksEntries(): ModelEntry[] {
53
+ return Object.entries(FireworksTextPricing)
54
+ .filter(([, pricing]) => !pricing.deprecated)
55
+ .map(([model]) => ({
56
+ provider: "fireworks",
57
+ model,
58
+ envKey: "FIREWORKS_API_KEY",
59
+ // Fireworks model support for reasoning_effort is model-specific. This
60
+ // suite exercises the request shape used by Patcher without that flag.
61
+ }));
62
+ }
63
+
50
64
  const TEST_MODELS: ModelEntry[] = [
51
65
  // OpenAI — all active chat + reasoning models + responses-API models
52
66
  // Deduplicate since some models appear in multiple lists
53
- ...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])]),
67
+ ...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
68
+ .map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
54
69
 
55
70
  // Anthropic — active text models (limited availability excluded)
56
- ...anthropicEntries(AnthropicTextModels),
71
+ ...anthropicEntries(AnthropicTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
57
72
 
58
73
  // Google — all active text models
59
- ...googleEntries(GoogleTextModels),
74
+ ...googleEntries(GoogleTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
60
75
 
61
76
  // XAI — active text models (deprecated grok-2 models excluded)
62
- ...xaiEntries(XaiTextModels),
77
+ ...xaiEntries(XaiTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
78
+
79
+ // Fireworks — all active catalog models
80
+ ...fireworksEntries(),
63
81
  ];
64
82
 
65
83
  // ─── Persistence helpers ──────────────────────────────────────────────────────
@@ -118,7 +136,7 @@ describe("Text Completions – multi-provider model verification", () => {
118
136
  console.log(` Total records: ${Object.keys(results).length}`);
119
137
  });
120
138
 
121
- for (const { provider, model, envKey } of TEST_MODELS) {
139
+ for (const { provider, model, envKey, reasoningEffort } of TEST_MODELS) {
122
140
  const key = recordKey(provider, model);
123
141
 
124
142
  test(`${provider} / ${model}`, async () => {
@@ -145,11 +163,10 @@ describe("Text Completions – multi-provider model verification", () => {
145
163
  model,
146
164
  messages: [{ role: "user", content: PROMPT }],
147
165
  max_tokens: 2000,
148
- // Use low reasoning effort for all models that support it — keeps latency
149
- // and cost down while still verifying the model responds successfully.
150
- // For OpenAI this maps to reasoning_effort="low", for Gemini to
151
- // thinkingLevel="low" or thinkingBudget=1024, etc.
152
- reasoning_effort: "low",
166
+ // Exercise the summary option enabled by Patcher. Provider clients must
167
+ // translate or remove this internal option rather than forwarding it.
168
+ reasoning_summary: true,
169
+ reasoning_effort: reasoningEffort,
153
170
  });
154
171
  const durationMs = Date.now() - startMs;
155
172
 
@@ -446,7 +446,7 @@ describe("TokenCompressor", () => {
446
446
  expect(result).toContain("Available keys:");
447
447
  });
448
448
 
449
- it("should auto-stitch a multi-chunk chain into the full content", () => {
449
+ it("should auto-stitch and paginate a multi-chunk chain", () => {
450
450
  // Force a multi-chunk compression then expand the first key.
451
451
  const content = "a".repeat(40000);
452
452
  const placeholder = tokenCompressor.compressStringInChunks(content);
@@ -456,9 +456,23 @@ describe("TokenCompressor", () => {
456
456
  const functions = toolsServiceCalls[0][0];
457
457
  const result = functions.expandTokens(firstKey);
458
458
 
459
- // Full content is returned in one call with no chunk-linking markers.
460
- expect(result).toBe(content);
459
+ expect(result.length).toBeLessThanOrEqual(20_000);
460
+ expect(result).toContain("Repeat with characterOffset=");
461
461
  expect(result).not.toContain("NEXT_CHUNK_KEY");
462
+
463
+ const pages = [result];
464
+ let offsetMatch = result.match(/characterOffset=(\d+)/);
465
+ while (offsetMatch) {
466
+ const page = functions.expandTokens(
467
+ firstKey,
468
+ undefined,
469
+ undefined,
470
+ Number(offsetMatch[1])
471
+ );
472
+ pages.push(page);
473
+ offsetMatch = page.match(/Repeat with characterOffset=(\d+)/);
474
+ }
475
+ expect(pages.reduce((sum, page) => sum + (page.match(/^a+/)?.[0].length || 0), 0)).toBe(content.length);
462
476
  });
463
477
 
464
478
  it("should support a ranged read with real line numbers", () => {
@@ -0,0 +1,68 @@
1
+ import {
2
+ executeJqQuery,
3
+ jqToolResponseDefinition,
4
+ } from "../../../src/processors/tools/jqToolResponse";
5
+ import {
6
+ executeTail,
7
+ tailToolResponseDefinition,
8
+ } from "../../../src/processors/tools/tailToolResponse";
9
+ import {
10
+ executeListStoredToolResponses,
11
+ listStoredToolResponsesDefinition,
12
+ } from "../../../src/processors/tools/listStoredToolResponses";
13
+
14
+ describe("bounded retrieval tools", () => {
15
+ it("bounds and paginates JQ output", async () => {
16
+ const data = JSON.stringify(Array.from({ length: 100 }, (_, i) => `value-${i}-${"x".repeat(30)}`));
17
+ const first = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, { maxCharacters: 1000 });
18
+ expect(first.length).toBeLessThanOrEqual(1000);
19
+ expect(first).toContain("Repeat with characterOffset=");
20
+
21
+ const offset = Number(first.match(/characterOffset=(\d+)/)?.[1]);
22
+ const next = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, {
23
+ characterOffset: offset,
24
+ maxCharacters: 1000,
25
+ });
26
+ expect(next).not.toBe(first);
27
+ });
28
+
29
+ it("bounds tail line lengths and supports backward pagination", async () => {
30
+ const data = ["first", `second ${"a".repeat(5000)}`, "third"].join("\n");
31
+ const result = await executeTail(data, "tail-id", ["tail-id"], {
32
+ lines: 1,
33
+ endLine: 2,
34
+ maxLineCharacters: 1000,
35
+ maxCharacters: 2000,
36
+ });
37
+ expect(result.length).toBeLessThanOrEqual(2000);
38
+ expect(result).toContain("Earlier lines available");
39
+ expect(result).toContain("lineCharacterOffset=1000");
40
+ });
41
+
42
+ it("paginates stored response results", async () => {
43
+ const storage = { a: "one", b: "two", c: "three" };
44
+ const metadata = {
45
+ a: { toolCallId: "a", originalLength: 3, storedAt: 1 },
46
+ b: { toolCallId: "b", originalLength: 3, storedAt: 2 },
47
+ c: { toolCallId: "c", originalLength: 5, storedAt: 3 },
48
+ };
49
+ const result = await executeListStoredToolResponses(storage, metadata, {}, {
50
+ maxResults: 1,
51
+ resultOffset: 1,
52
+ });
53
+ expect(result).toContain("Tool Call ID: b");
54
+ expect(result).not.toContain("Tool Call ID: c");
55
+ expect(result).toContain("resultOffset=2");
56
+ });
57
+
58
+ it("exposes pagination controls in each tool schema", () => {
59
+ const jqOptions = (jqToolResponseDefinition.function.parameters.properties as any).options;
60
+ const tailOptions = (tailToolResponseDefinition.function.parameters.properties as any).options;
61
+ const listOptions = (listStoredToolResponsesDefinition.function.parameters.properties as any).options;
62
+ expect(jqOptions.properties.characterOffset).toBeDefined();
63
+ expect(tailOptions.properties.endLine).toBeDefined();
64
+ expect(tailOptions.properties.maxCharacters).toBeDefined();
65
+ expect(listOptions.properties.resultOffset).toBeDefined();
66
+ expect(listOptions.properties.maxCharacters).toBeDefined();
67
+ });
68
+ });
@@ -1,4 +1,7 @@
1
- import { executeGrep } from "../../../src/processors/tools/grepToolResponse";
1
+ import {
2
+ executeGrep,
3
+ grepToolResponseDefinition,
4
+ } from "../../../src/processors/tools/grepToolResponse";
2
5
 
3
6
  /**
4
7
  * Verifies grepToolResponse operates on the decompressed/plain stored content
@@ -69,4 +72,88 @@ describe("executeGrep", () => {
69
72
 
70
73
  expect(result).toContain("No matches found");
71
74
  });
75
+
76
+ it("bounds the total response and advertises the next result page", async () => {
77
+ const manyMatches = Array.from(
78
+ { length: 20 },
79
+ (_, index) => `match ${index} ${"x".repeat(200)}`
80
+ ).join("\n");
81
+
82
+ const result = await executeGrep(
83
+ manyMatches,
84
+ toolCallId,
85
+ "match",
86
+ [toolCallId],
87
+ { maxCharacters: 1000, maxResults: 20 }
88
+ );
89
+
90
+ expect(result.length).toBeLessThanOrEqual(1000);
91
+ expect(result).toContain("More matches available");
92
+ expect(result).toMatch(/resultOffset=\d+/);
93
+ });
94
+
95
+ it("keeps a large log grep under the default response limit", async () => {
96
+ const logMatches = Array.from(
97
+ { length: 500 },
98
+ (_, index) => `${index}: ${JSON.stringify({ level: 30, message: "sandbox runner", detail: "x".repeat(500) })}`
99
+ ).join("\n");
100
+
101
+ const result = await executeGrep(
102
+ logMatches,
103
+ toolCallId,
104
+ "runner|sandbox",
105
+ [toolCallId]
106
+ );
107
+
108
+ expect(result.length).toBeLessThanOrEqual(20_000);
109
+ expect(result).toContain("More matches available");
110
+ });
111
+
112
+ it("paginates matches using resultOffset", async () => {
113
+ const result = await executeGrep(
114
+ ["match zero", "match one", "match two"].join("\n"),
115
+ toolCallId,
116
+ "match",
117
+ [toolCallId],
118
+ { maxResults: 1, resultOffset: 1 }
119
+ );
120
+
121
+ expect(result).not.toContain("match zero");
122
+ expect(result).toContain("match one");
123
+ expect(result).not.toContain("match two");
124
+ expect(result).toContain("resultOffset=2");
125
+ });
126
+
127
+ it("slices very long matching lines and lets callers page within the line", async () => {
128
+ const longLine = `prefix-${"a".repeat(5000)}-suffix`;
129
+ const firstPage = await executeGrep(
130
+ longLine,
131
+ toolCallId,
132
+ "prefix",
133
+ [toolCallId]
134
+ );
135
+
136
+ expect(firstPage.length).toBeLessThan(10_000);
137
+ expect(firstPage).toContain("line 1 truncated");
138
+ expect(firstPage).toContain("lineCharacterOffset=4000");
139
+ expect(firstPage).not.toContain("-suffix");
140
+
141
+ const secondPage = await executeGrep(
142
+ longLine,
143
+ toolCallId,
144
+ "prefix",
145
+ [toolCallId],
146
+ { lineCharacterOffset: 4000 }
147
+ );
148
+
149
+ expect(secondPage).toContain("-suffix");
150
+ expect(secondPage).toContain("End of matches");
151
+ });
152
+
153
+ it("exposes pagination controls in the tool schema", () => {
154
+ const options = (grepToolResponseDefinition.function.parameters.properties as any).options;
155
+ expect(options.properties.resultOffset).toBeDefined();
156
+ expect(options.properties.lineCharacterOffset).toBeDefined();
157
+ expect(options.properties.maxCharacters).toBeDefined();
158
+ });
72
159
  });