@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.145",
3
+ "version": "0.0.146",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -43,6 +43,22 @@ export class CliChatService implements ChatService {
43
43
  chatHistory: this.chatHistory,
44
44
  plugins,
45
45
  };
46
+ this.registerCommand({
47
+ name: "reload",
48
+ description: "Reload configuration, tools, MCP connections, and modules",
49
+ handler: async () => {
50
+ try {
51
+ console.log("Reloading configuration...");
52
+ const result = await services().RuntimeReload.reload();
53
+ console.log(
54
+ `✓ Reloaded ${result.tools} tools, ${result.mcps} MCP servers, and ${result.modules} modules`
55
+ );
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ console.error(`⚠ Reload failed: ${message}`);
59
+ }
60
+ },
61
+ });
46
62
  this.loadInputHistory();
47
63
 
48
64
  // Set up callback to add entries to inputHistory immediately when user presses Enter
@@ -18,7 +18,12 @@ import { BaseChatModule } from "./BaseChatModule";
18
18
  import { services } from "../../services/index";
19
19
  import { BaseAgent } from "../../agents/index";
20
20
  import { ChatCommand, ChatMode, ChatContext, ChatService } from "../types";
21
- import { Message, CompletionOptions } from "../../clients/types";
21
+ import {
22
+ Message,
23
+ CompletionOptions,
24
+ REASONING_EFFORTS,
25
+ isReasoningEffort,
26
+ } from "../../clients/types";
22
27
  import { ChatInteraction } from "../../types";
23
28
  import { Marked } from "../../utils/index";
24
29
  import { TokenCompressor } from "../../processors/TokenCompressor";
@@ -173,21 +178,15 @@ export class AgentModule extends BaseChatModule {
173
178
  {
174
179
  name: "reasoning_effort",
175
180
  description:
176
- "Set the reasoning effort for the agent (none, low, medium, high). Use 'none' to disable thinking on models that support it.",
181
+ `Set the reasoning effort for the agent (${REASONING_EFFORTS.join(", ")}). Individual models support different subsets.`,
177
182
  handler: async (args: string[]): Promise<void> => {
178
- const validEfforts: CompletionOptions["reasoning_effort"][] = [
179
- "none",
180
- "low",
181
- "medium",
182
- "high",
183
- ];
184
- const effort = args[0] as CompletionOptions["reasoning_effort"];
185
- if (!effort || !validEfforts.includes(effort)) {
183
+ const effort = args[0];
184
+ if (!isReasoningEffort(effort)) {
186
185
  console.log(
187
186
  `Current reasoning effort: ${this.reasoningEffort ?? "(not set — model default)"}`
188
187
  );
189
188
  console.log(
190
- `Usage: /reasoning_effort <${validEfforts.join("|")}>`
189
+ `Usage: /reasoning_effort <${REASONING_EFFORTS.join("|")}>`
191
190
  );
192
191
  return;
193
192
  }
@@ -201,19 +200,13 @@ export class AgentModule extends BaseChatModule {
201
200
  name: "effort",
202
201
  description: "Alias for /reasoning_effort",
203
202
  handler: async (args: string[]): Promise<void> => {
204
- const validEfforts: CompletionOptions["reasoning_effort"][] = [
205
- "none",
206
- "low",
207
- "medium",
208
- "high",
209
- ];
210
- const effort = args[0] as CompletionOptions["reasoning_effort"];
211
- if (!effort || !validEfforts.includes(effort)) {
203
+ const effort = args[0];
204
+ if (!isReasoningEffort(effort)) {
212
205
  console.log(
213
206
  `Current reasoning effort: ${this.reasoningEffort ?? "(not set — model default)"}`
214
207
  );
215
208
  console.log(
216
- `Usage: /effort <${validEfforts.join("|")}>`
209
+ `Usage: /effort <${REASONING_EFFORTS.join("|")}>`
217
210
  );
218
211
  return;
219
212
  }
@@ -247,8 +240,15 @@ export class AgentModule extends BaseChatModule {
247
240
  description: "Detach from the currently attached agent",
248
241
  modes: ["agent:attached"],
249
242
  handler: async (_args: string[]): Promise<void> => {
243
+ const agent = this.attachedAgent;
250
244
  console.log("Detached from agent");
251
- this.detachFromAgent();
245
+ // Only detach if we are still attached to THIS agent. If the user
246
+ // killed this agent and started a new one while this agent was
247
+ // finishing a long tool call, detachFromAgent() would rip out the
248
+ // new agent's rendering and reset the mode.
249
+ if (agent && this.attachedAgent === agent) {
250
+ this.detachFromAgent();
251
+ }
252
252
  },
253
253
  },
254
254
  {
@@ -1468,8 +1468,15 @@ export class AgentModule extends BaseChatModule {
1468
1468
  }
1469
1469
 
1470
1470
  resolve("done");
1471
- // Exit agent:attached mode so the prompt resets back to the default
1472
- this.detachFromAgent();
1471
+ // Exit agent:attached mode so the prompt resets back to the default.
1472
+ // Only detach if we are still attached to THIS agent. If the user
1473
+ // killed this agent and started a new one (or /attached to a different
1474
+ // one) while this agent was finishing a long tool call, calling
1475
+ // detachFromAgent() unconditionally would rip out the new agent's
1476
+ // rendering listeners and reset the mode, making new output invisible.
1477
+ if (this.attachedAgent === agent) {
1478
+ this.detachFromAgent();
1479
+ }
1473
1480
  });
1474
1481
  });
1475
1482
 
@@ -214,7 +214,6 @@ export class SessionsModule extends BaseChatModule {
214
214
  // ── Case 1: in-memory running task ──────────────────────────────────────
215
215
  if (taskRegistry.has(id)) {
216
216
  const taskInfo = taskRegistry.get(id)!;
217
- const renderer = this.agentModule.getRenderer();
218
217
  const context = this.chatService?.getContext();
219
218
  const selectedAgent = taskInfo.agent;
220
219
 
@@ -222,13 +221,9 @@ export class SessionsModule extends BaseChatModule {
222
221
  context.selectedAgent = selectedAgent;
223
222
  context.agentMode = true;
224
223
  context.currentAgent = taskInfo.agentName;
225
- context.activeAgentTaskId = id;
226
224
  context.currentModel = selectedAgent.getModel();
227
225
  context.currentProvider = selectedAgent.getProvider();
228
226
  }
229
- this.agentModule.setActiveAgentTaskId(id);
230
- renderer.setActiveTaskId(id);
231
- if (this.chatService) this.chatService.setMode("agent:attached");
232
227
 
233
228
  console.log(`🔄 Attached to running task: ${id}`);
234
229
  console.log(` Agent : ${taskInfo.agentName}`);
@@ -237,6 +232,13 @@ export class SessionsModule extends BaseChatModule {
237
232
  console.log(
238
233
  ` Type /logs to see recent messages, or /detach to detach.`
239
234
  );
235
+
236
+ // Route through attachedAgentChatLoop so that:
237
+ // 1. wireAgentRendering is called (new output becomes visible)
238
+ // 2. attachedAgent is set (so /poke, /kill, /compact work)
239
+ // 3. a done listener is registered (mode resets when agent finishes)
240
+ // Do NOT pass initialInput — agent is already running, no need to call it again.
241
+ await this.agentModule.attachedAgentChatLoop(id, selectedAgent as any);
240
242
  return;
241
243
  }
242
244
 
package/src/chat/types.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { ChatInteraction, Config } from "../types";
5
5
  import { BaseAgent } from "../agents/base/base";
6
+ import { ReasoningEffort } from "../clients/types";
6
7
  import { ToolsService } from "../services";
7
8
  import { AgentRenderer } from "./renderer/types";
8
9
 
@@ -101,7 +102,7 @@ export interface TaskInfo {
101
102
  // restore the exact configuration instead of falling back to agent defaults.
102
103
  model?: string;
103
104
  provider?: string;
104
- reasoningEffort?: "none" | "low" | "medium" | "high";
105
+ reasoningEffort?: ReasoningEffort;
105
106
  summarizeReasoning?: boolean;
106
107
  }
107
108
 
@@ -123,7 +124,7 @@ export interface ChatSession {
123
124
  // Persisted run configuration for exact resume restoration.
124
125
  model?: string;
125
126
  provider?: string;
126
- reasoningEffort?: "none" | "low" | "medium" | "high";
127
+ reasoningEffort?: ReasoningEffort;
127
128
  summarizeReasoning?: boolean;
128
129
  }
129
130
 
@@ -88,6 +88,25 @@ export class GenericAnthropicClient implements GenericClient {
88
88
  return undefined;
89
89
  }
90
90
 
91
+ /** Map provider-neutral effort names to Anthropic's output_config.effort. */
92
+ getEffortOutputConfig(
93
+ model: string,
94
+ reasoningEffort?: CompletionOptions["reasoning_effort"]
95
+ ): Anthropic.OutputConfig | undefined {
96
+ if (
97
+ reasoningEffort === undefined ||
98
+ reasoningEffort === "none" ||
99
+ (!alwaysOnModels.includes(model) && !optInModels.includes(model))
100
+ ) {
101
+ return undefined;
102
+ }
103
+
104
+ return {
105
+ // Anthropic uses `low` for the provider-neutral `minimal` level.
106
+ effort: reasoningEffort === "minimal" ? "low" : reasoningEffort,
107
+ };
108
+ }
109
+
91
110
  /**
92
111
  * Clean JSON Schema for Anthropic API compatibility.
93
112
  * Removes unsupported fields like additionalProperties, $ref, $defs, positional.
@@ -423,6 +442,10 @@ export class GenericAnthropicClient implements GenericClient {
423
442
  options.reasoning_effort,
424
443
  options.reasoning_summary,
425
444
  );
445
+ const effortOutputConfig = this.getEffortOutputConfig(
446
+ options.model,
447
+ options.reasoning_effort
448
+ );
426
449
 
427
450
  const systemMessage = options.messages
428
451
  .filter((msg) => msg.role === "system")
@@ -447,6 +470,7 @@ export class GenericAnthropicClient implements GenericClient {
447
470
  : undefined,
448
471
  max_tokens: options.max_tokens || 8000,
449
472
  ...(thinkingParam && { thinking: thinkingParam }),
473
+ ...(effortOutputConfig && { output_config: effortOutputConfig }),
450
474
  ...(tools.length && {
451
475
  tool_choice: { type: "auto" },
452
476
  tools,
@@ -51,6 +51,9 @@ export class GenericFireworksClient extends HttpClient {
51
51
  ...options,
52
52
  // Strip Anthropic-specific field not accepted by Fireworks
53
53
  long_ttl_cache: undefined,
54
+ // This is translated by the OpenAI/Anthropic clients, but is not part of
55
+ // Fireworks' OpenAI-compatible chat completion schema.
56
+ reasoning_summary: undefined,
54
57
  };
55
58
 
56
59
  if (sanitized.tools) {
@@ -434,9 +434,13 @@ export class GenericGeminiClient implements GenericClient {
434
434
  // Gemini 3.x — use thinkingLevel
435
435
  if (GoogleThinkingLevelModels.includes(model)) {
436
436
  const levelMap: Record<string, string> = {
437
+ none: "minimal",
438
+ minimal: "minimal",
437
439
  low: "low",
438
440
  medium: "medium",
439
441
  high: "high",
442
+ xhigh: "high",
443
+ max: "high",
440
444
  };
441
445
  return {
442
446
  thinkingLevel: levelMap[effort] ?? "low",
@@ -447,9 +451,13 @@ export class GenericGeminiClient implements GenericClient {
447
451
  if (GoogleThinkingBudgetModels.includes(model)) {
448
452
  // Map effort to token budget
449
453
  const budgetMap: Record<string, number> = {
454
+ none: 0,
455
+ minimal: 512,
450
456
  low: 1024,
451
457
  medium: 8192,
452
458
  high: -1, // dynamic
459
+ xhigh: -1,
460
+ max: -1,
453
461
  };
454
462
  return {
455
463
  thinkingBudget: budgetMap[effort] ?? 1024,
@@ -112,9 +112,10 @@ export class GenericOpenAiClient implements GenericClient {
112
112
 
113
113
  resolveReasoningEffort(options: CompletionOptions): "low" | "medium" | "high" {
114
114
  const effort = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
115
- // "none" is not a valid SDK ReasoningEffort — callers that need "none" should
116
- // check isReasoningDisabled() separately and skip the reasoning params.
117
- if (effort === "none") return "low";
115
+ // Chat Completions only accepts low/medium/high. Preserve newer values on
116
+ // the Responses API, but map them to the nearest legacy level here.
117
+ if (effort === "none" || effort === "minimal") return "low";
118
+ if (effort === "xhigh" || effort === "max") return "high";
118
119
  return effort;
119
120
  }
120
121
 
@@ -125,7 +126,9 @@ export class GenericOpenAiClient implements GenericClient {
125
126
  */
126
127
  resolveReasoningEffortForModel(options: CompletionOptions): string {
127
128
  const raw = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
128
- const requested = raw === "none" ? "none" : raw;
129
+ // Anthropic calls its highest level `max`; OpenAI calls it `xhigh`.
130
+ // Accept both provider-neutral CLI spellings.
131
+ const requested = raw === "max" ? "xhigh" : raw;
129
132
  const pricing = OpenAiTextPricing[options.model];
130
133
  const supportedLevels = pricing?.reasoningLevels;
131
134
  if (!supportedLevels || supportedLevels.length === 0) {
@@ -136,6 +136,26 @@ export interface RetryOptions {
136
136
  signal?: AbortSignal;
137
137
  }
138
138
 
139
+ /**
140
+ * Every reasoning-effort spelling currently exposed by a supported provider.
141
+ * Individual models may support only a subset; provider clients are responsible
142
+ * for translating or clamping values where their APIs require it.
143
+ */
144
+ export const REASONING_EFFORTS = [
145
+ "none",
146
+ "minimal",
147
+ "low",
148
+ "medium",
149
+ "high",
150
+ "xhigh",
151
+ "max",
152
+ ] as const;
153
+ export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
154
+
155
+ export function isReasoningEffort(value: unknown): value is ReasoningEffort {
156
+ return typeof value === "string" && REASONING_EFFORTS.includes(value as ReasoningEffort);
157
+ }
158
+
139
159
  export interface CompletionOptions extends RetryOptions {
140
160
  model: string;
141
161
  messages: Message[];
@@ -147,14 +167,17 @@ export interface CompletionOptions extends RetryOptions {
147
167
  * Maps to: OpenAI reasoning_effort/reasoning.effort, xAI reasoning.effort,
148
168
  * Gemini thinkingLevel/thinkingBudget, Anthropic thinking mode.
149
169
  *
150
- * "none" = disable thinking entirely (Anthropic: thinking.type="disabled",
151
- * OpenAI: reasoning.effort="none"). Only supported by models that
152
- * allow disabling thinking (e.g. claude-sonnet-5).
153
- * "low" = minimal thinking
154
- * "medium" = balanced (default for most models)
155
- * "high" = maximum reasoning
170
+ * "none" = disable thinking where supported
171
+ * "minimal" = provider-defined minimum (for example Gemini/OpenAI)
172
+ * "low" = low reasoning
173
+ * "medium" = balanced reasoning
174
+ * "high" = high reasoning
175
+ * "xhigh" = extra-high reasoning (for example OpenAI/xAI/Anthropic)
176
+ * "max" = maximum reasoning (Anthropic)
177
+ *
178
+ * A model may support only a subset of these provider-level values.
156
179
  */
157
- reasoning_effort?: "none" | "low" | "medium" | "high";
180
+ reasoning_effort?: ReasoningEffort;
158
181
  /**
159
182
  * When true, request a summarized view of the model's thinking process.
160
183
  * - Anthropic: thinking.display = "summarized"
@@ -52,6 +52,15 @@ export class GenericXAIClient implements GenericClient {
52
52
  });
53
53
  }
54
54
 
55
+ resolveLegacyReasoningEffort(
56
+ effort: CompletionOptions["reasoning_effort"]
57
+ ): "low" | "medium" | "high" | undefined {
58
+ if (effort === undefined || effort === "none") return undefined;
59
+ if (effort === "minimal") return "low";
60
+ if (effort === "xhigh" || effort === "max") return "high";
61
+ return effort;
62
+ }
63
+
55
64
  async createChatCompletion(
56
65
  options: CompletionOptions
57
66
  ): Promise<CompletionResponse> {
@@ -72,13 +81,14 @@ export class GenericXAIClient implements GenericClient {
72
81
  return msg as ChatCompletionMessageParam;
73
82
  });
74
83
 
84
+ const reasoningEffort = this.resolveLegacyReasoningEffort(options.reasoning_effort);
75
85
  const response = await this.client.chat.completions.create({
76
86
  model: options.model,
77
87
  messages: xaiMessages,
78
88
  max_tokens: options.max_tokens,
79
- ...(XaiReasoningModels.includes(options.model) && options.reasoning_effort && options.reasoning_effort !== "none" && {
89
+ ...(XaiReasoningModels.includes(options.model) && reasoningEffort && {
80
90
  // grok-3-mini models support reasoning_effort: "low" | "medium" | "high"
81
- reasoning_effort: options.reasoning_effort as "low" | "medium" | "high",
91
+ reasoning_effort: reasoningEffort,
82
92
  }),
83
93
  ...(options.tools && {
84
94
  tools: options.tools,
@@ -206,8 +216,17 @@ export class GenericXAIClient implements GenericClient {
206
216
  // Resolve reasoning effort, clamping to supported levels if defined in pricing
207
217
  const pricing = XaiTextPricing[options.model];
208
218
  const supportedLevels = pricing?.reasoningLevels;
209
- let reasoningEffort: string | undefined = options.reasoning_effort;
210
- if (supportedLevels?.length) {
219
+ const requestedEffort = options.reasoning_effort;
220
+ let reasoningEffort: string | undefined;
221
+ if (requestedEffort !== "none") {
222
+ reasoningEffort =
223
+ requestedEffort === "minimal"
224
+ ? "low"
225
+ : requestedEffort === "max"
226
+ ? "xhigh"
227
+ : requestedEffort;
228
+ }
229
+ if (requestedEffort !== "none" && supportedLevels?.length) {
211
230
  if (!reasoningEffort || !supportedLevels.includes(reasoningEffort)) {
212
231
  reasoningEffort = supportedLevels[0];
213
232
  }
@@ -8,6 +8,7 @@ import { SessionsModule } from "../chat/modules/SessionsModule";
8
8
  import { SetupModule } from "../chat/modules/SetupModule";
9
9
  import { PlainRenderer } from "../chat/renderer/PlainRenderer";
10
10
  import { loadRenderer } from "../chat/renderer/loadRenderer";
11
+ import { isReasoningEffort, REASONING_EFFORTS } from "../clients/types";
11
12
  import { getConfig } from "../config";
12
13
 
13
14
  async function setupRenderer(chatService: any, rendererSpecifier: string): Promise<void> {
@@ -57,7 +58,7 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
57
58
  .option("--agent-name <name>", "Which agent to use", "Patcher")
58
59
  .option(
59
60
  "--reasoning-effort <effort>",
60
- "Reasoning effort: none, low, medium, or high. Use 'none' to disable thinking on models that support it (e.g. for faster computer-use)."
61
+ `Reasoning effort: ${REASONING_EFFORTS.join(", ")}. Individual models support different subsets.`
61
62
  )
62
63
  .option(
63
64
  "--max-time-limit <minutes>",
@@ -103,6 +104,14 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
103
104
  )
104
105
  .action(async (options) => {
105
106
  try {
107
+ if (
108
+ options.reasoningEffort !== undefined &&
109
+ !isReasoningEffort(options.reasoningEffort)
110
+ ) {
111
+ throw new Error(
112
+ `Invalid reasoning effort "${options.reasoningEffort}". Expected one of: ${REASONING_EFFORTS.join(", ")}.`
113
+ );
114
+ }
106
115
  const { setupServices } = await import("./services");
107
116
  await setupServices();
108
117
  const chatService = getChatService();
@@ -1,6 +1,6 @@
1
1
  import { includedTools } from "../agents/tools/list";
2
2
  import * as allTools from "../agents/tools";
3
- import { LazyToolsService, services, MinimalToolsService } from "../services";
3
+ import { LazyToolsService, services, MinimalToolsService, TracingService } from "../services";
4
4
  import { agents } from "../agents";
5
5
  import { ModulesService } from "../services/modules";
6
6
  import { getConfig } from "../config";
@@ -19,8 +19,10 @@ export async function setupServices() {
19
19
  Events,
20
20
  MediaProcessor,
21
21
  Behaviors,
22
+ RuntimeReload,
22
23
  } = services();
23
24
 
25
+ await Plugins.refreshConfiguredState();
24
26
  // cli uses LazyTools to keep context slim
25
27
  const Tools = new LazyToolsService();
26
28
  await Behaviors.initFromConfig();
@@ -80,7 +82,7 @@ export async function setupServices() {
80
82
 
81
83
  console.log("📦 Loading modules from config...");
82
84
  const modulesService = new ModulesService();
83
- await modulesService.loadModulesFromConfig({
85
+ const moduleContext = {
84
86
  Agents,
85
87
  Embeddings,
86
88
  Plugins,
@@ -89,7 +91,51 @@ export async function setupServices() {
89
91
  MediaProcessor,
90
92
  Behaviors,
91
93
  Events,
94
+ Tracing: TracingService,
95
+ };
96
+
97
+ // Call destroy() on all modules when the process is shutting down so they
98
+ // can flush buffers (e.g. OTEL spans), close connections, etc.
99
+ const destroyAll = async () => {
100
+ await modulesService.destroyModules();
101
+ };
102
+ process.on("beforeExit", destroyAll);
103
+ process.on("SIGINT", async () => {
104
+ await destroyAll();
105
+ process.exit(0);
106
+ });
107
+ await modulesService.loadModulesFromConfig(moduleContext);
108
+
109
+ RuntimeReload.configure(async () => {
110
+ await modulesService.destroyModules();
111
+ await Plugins.refreshConfiguredState();
112
+ await Mcp.closeAll();
113
+
114
+ Tools.resetTools();
115
+ Tools.defineTools(includedTools, allTools);
116
+ try {
117
+ const agentCallDef = AllTools.getTool?.("agentCall");
118
+ const agentCallFn = AllTools.getFunction?.("agentCall");
119
+ if (agentCallDef) Tools.addTool(agentCallDef);
120
+ if (agentCallFn) Tools.setFunction("agentCall", agentCallFn);
121
+ } catch (_) {}
122
+
123
+ await Behaviors.initFromConfig();
124
+ await Mcp.connectToConfigured(Tools);
125
+ await Clients.registerConfiguredModels();
126
+ await modulesService.loadModulesFromConfig(moduleContext);
127
+
128
+ const config = await getConfig();
129
+ return {
130
+ tools: Tools.getTools().length,
131
+ mcps: (config.mcps || []).length,
132
+ modules: (config.modules || []).length,
133
+ };
134
+ });
135
+ process.on("SIGTERM", async () => {
136
+ await destroyAll();
137
+ process.exit(0);
92
138
  });
93
139
 
94
- return { Tools, Clients };
140
+ return { Tools, Clients, modulesService };
95
141
  }
@@ -52,6 +52,22 @@ export class PluginService {
52
52
  return true;
53
53
  }
54
54
 
55
+ /** Apply the enabled/disabled state from the current Knowhow config. */
56
+ async refreshConfiguredState(): Promise<void> {
57
+ const config = await getConfig();
58
+ const disabled = new Set(getDisabledPlugins(config.plugins));
59
+
60
+ for (const [key, plugin] of this.pluginMap) {
61
+ if (disabled.has(key)) {
62
+ plugin.disable();
63
+ } else {
64
+ // Configuration is authoritative when services start or reload. This
65
+ // also re-enables a plugin after it is removed from `disabled`.
66
+ plugin.enable();
67
+ }
68
+ }
69
+ }
70
+
55
71
  /* -------- existing public API (updated for compatibility) ---------------------- */
56
72
 
57
73
  listPlugins() {
@@ -7,6 +7,7 @@ import {
7
7
  CompressionMetadata,
8
8
  JsonCompressorStorage,
9
9
  } from "./JsonCompressor";
10
+ import { paginateToolResponse } from "./tools/boundedToolResponse";
10
11
 
11
12
  export interface KeyInfo {
12
13
  key: string;
@@ -419,15 +420,25 @@ export class TokenCompressor implements JsonCompressorStorage {
419
420
  if (toolsService) {
420
421
  toolsService.addTools([expandTokensDefinition]);
421
422
  toolsService.addFunctions({
422
- [this.toolName]: (key: string, fromLine?: number, toLine?: number) => {
423
+ [this.toolName]: (
424
+ key: string,
425
+ fromLine?: number,
426
+ toLine?: number,
427
+ characterOffset?: number,
428
+ maxCharacters?: number
429
+ ) => {
423
430
  // Auto-stitch: follow any NEXT_CHUNK_KEY chain and return the full,
424
431
  // reassembled content so the agent never has to chase nested keys.
425
432
  const data = this.retrieveFullString(key);
426
433
 
427
434
  if (data === null) {
428
- return `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
429
- ", "
430
- )}`;
435
+ return paginateToolResponse(
436
+ `Error: No data found for key "${key}". Available keys: ${this.getStorageKeys().join(
437
+ ", "
438
+ )}`,
439
+ { characterOffset, maxCharacters },
440
+ "available-key list"
441
+ );
431
442
  }
432
443
 
433
444
  // Optional ranged read: return only the requested 1-based, inclusive
@@ -435,7 +446,11 @@ export class TokenCompressor implements JsonCompressorStorage {
435
446
  const hasRange =
436
447
  typeof fromLine === "number" || typeof toLine === "number";
437
448
  if (!hasRange) {
438
- return data;
449
+ return paginateToolResponse(
450
+ data,
451
+ { characterOffset, maxCharacters },
452
+ `expanded value for ${key}`
453
+ );
439
454
  }
440
455
 
441
456
  const lines = data.split("\n");
@@ -447,14 +462,22 @@ export class TokenCompressor implements JsonCompressorStorage {
447
462
  );
448
463
 
449
464
  if (start > end) {
450
- return `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`;
465
+ return paginateToolResponse(
466
+ `Error: Invalid line range for key "${key}": fromLine (${start}) is greater than toLine (${end}). Content has ${totalLines} lines.`,
467
+ { characterOffset, maxCharacters },
468
+ "line-range error"
469
+ );
451
470
  }
452
471
 
453
472
  const numbered: string[] = [];
454
473
  for (let i = start; i <= end; i++) {
455
474
  numbered.push(`${i}: ${lines[i - 1]}`);
456
475
  }
457
- return numbered.join("\n");
476
+ return paginateToolResponse(
477
+ numbered.join("\n"),
478
+ { characterOffset, maxCharacters },
479
+ `expanded lines ${start}-${end} for ${key}`
480
+ );
458
481
  },
459
482
  });
460
483
  }
@@ -559,7 +582,7 @@ export const expandTokensDefinition: Tool = {
559
582
  function: {
560
583
  name: "expandTokens",
561
584
  description:
562
- "Retrieve compressed data that was stored during message processing. The full content is automatically reassembled (any chunk chain is followed for you, so you never need to chase NEXT_CHUNK_KEY references). Optionally pass fromLine/toLine (1-based, inclusive) to return just a range of lines, prefixed with real line numbers. NOTE: Can also use jqToolResponse, grepToolResponse, tailToolResponse to access/filter/search compressed content without needing to expand all tokens.",
585
+ "Retrieve a bounded page of compressed data. Chunk chains are automatically reassembled, but output is limited to 20,000 characters by default. Use characterOffset to continue, or fromLine/toLine for line-based selection. Prefer jqToolResponse, grepToolResponse, or tailToolResponse when possible.",
563
586
  parameters: {
564
587
  type: "object",
565
588
  positional: true,
@@ -578,6 +601,15 @@ export const expandTokensDefinition: Tool = {
578
601
  description:
579
602
  "Optional 1-based end line (inclusive). Defaults to the end of the content when omitted.",
580
603
  },
604
+ characterOffset: {
605
+ type: "number",
606
+ description:
607
+ "Optional zero-based character offset within the expanded value or selected line range.",
608
+ },
609
+ maxCharacters: {
610
+ type: "number",
611
+ description: "Response size (default 20000, hard maximum 50000).",
612
+ },
581
613
  },
582
614
  required: ["key"],
583
615
  },