@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
@@ -1,7 +1,13 @@
1
1
  import { Tool } from "../../clients";
2
+ import { boundedInteger, paginateToolResponse } from "./boundedToolResponse";
2
3
 
3
4
  export interface TailOptions {
4
5
  lines?: number;
6
+ endLine?: number;
7
+ lineCharacterOffset?: number;
8
+ maxLineCharacters?: number;
9
+ characterOffset?: number;
10
+ maxCharacters?: number;
5
11
  }
6
12
 
7
13
  /**
@@ -21,31 +27,71 @@ export async function executeTail(
21
27
  return name ? `${id} (${name})` : id;
22
28
  })
23
29
  .join("\n - ");
24
- return `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`;
30
+ return paginateToolResponse(
31
+ `Error: No tool response found for toolCallId "${toolCallId}". Call listStoredToolResponses to see all available responses with their tool names.\n\nAvailable toolCallIds:\n - ${idList || "(none)"}`,
32
+ {
33
+ characterOffset: options?.characterOffset,
34
+ maxCharacters: options?.maxCharacters,
35
+ },
36
+ "available toolCallId list"
37
+ );
25
38
  }
26
39
 
27
40
  try {
28
41
  const lines = data.split("\n");
29
- const numLines = options?.lines ?? 10;
42
+ const numLines = boundedInteger(options?.lines, 10, 0, 1000);
30
43
 
31
- // Handle edge case: 0 lines requested
32
44
  if (numLines <= 0) {
33
45
  return "";
34
46
  }
35
47
 
36
- // Get the last n lines
37
- const startIdx = Math.max(0, lines.length - numLines);
38
- const tailLines = lines.slice(startIdx);
48
+ const endLine = boundedInteger(options?.endLine, lines.length, 1, lines.length);
49
+ const startIdx = Math.max(0, endLine - numLines);
50
+ const lineCharacterOffset = boundedInteger(
51
+ options?.lineCharacterOffset,
52
+ 0,
53
+ 0,
54
+ Number.MAX_SAFE_INTEGER
55
+ );
56
+ const maxLineCharacters = boundedInteger(
57
+ options?.maxLineCharacters,
58
+ 4000,
59
+ 100,
60
+ 10_000
61
+ );
62
+ const longLines: number[] = [];
39
63
 
40
- // Format with line numbers
41
- const formatted = tailLines.map((line, idx) => {
42
- const lineNum = startIdx + idx + 1;
43
- return `${lineNum}: ${line}`;
64
+ const formatted = lines.slice(startIdx, endLine).map((line, idx) => {
65
+ const lineNumber = startIdx + idx + 1;
66
+ const visible = line.slice(
67
+ lineCharacterOffset,
68
+ lineCharacterOffset + maxLineCharacters
69
+ );
70
+ if (lineCharacterOffset + maxLineCharacters < line.length) {
71
+ longLines.push(lineNumber);
72
+ }
73
+ return `${lineNumber}: ${visible}`;
44
74
  });
45
75
 
46
- return formatted.join("\n");
47
- } catch (error: any) {
48
- return `Tail Error: ${error.message}`;
76
+ const notes: string[] = [];
77
+ if (typeof options?.endLine === "number" && startIdx > 0) {
78
+ notes.push(`[Earlier lines available: repeat with options.endLine=${startIdx}.]`);
79
+ }
80
+ if (longLines.length > 0) notes.push(`[Long lines ${longLines.join(", ")} were sliced; repeat with options.lineCharacterOffset=${lineCharacterOffset + maxLineCharacters}.]`);
81
+ const rendered =
82
+ formatted.join("\n") +
83
+ (notes.length > 0 ? `\n\n${notes.join("\n")}` : "");
84
+ return paginateToolResponse(
85
+ rendered,
86
+ {
87
+ characterOffset: options?.characterOffset,
88
+ maxCharacters: options?.maxCharacters,
89
+ },
90
+ "tail result"
91
+ );
92
+ } catch (error: unknown) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ return `Tail Error: ${message.slice(0, 500)}`;
49
95
  }
50
96
  }
51
97
 
@@ -54,22 +100,44 @@ export const tailToolResponseDefinition: Tool = {
54
100
  function: {
55
101
  name: "tailToolResponse",
56
102
  description:
57
- "Get the last n lines from a stored tool response. Similar to the Unix 'tail' command, useful when a tool response is too large and you only need to see the end. Returns the last n lines with line numbers.",
103
+ "Get a bounded page of lines from the end of a stored tool response. Use endLine to page backward and lineCharacterOffset to page through unusually long lines.",
58
104
  parameters: {
59
105
  type: "object",
60
106
  positional: true,
61
107
  properties: {
62
108
  toolCallId: {
63
109
  type: "string",
64
- description: "The toolCallId of the stored tool response",
110
+ description: "The toolCallId of the stored tool response.",
65
111
  },
66
112
  options: {
67
113
  type: "object",
68
- description: "Optional tail settings: lines (number, default: 10) - number of lines to return from the end",
114
+ description: "Optional tail and pagination settings.",
69
115
  properties: {
70
116
  lines: {
71
117
  type: "number",
72
- description: "Number of lines to return from the end (default: 10)",
118
+ description: "Lines per page (default 10, maximum 1000).",
119
+ },
120
+ endLine: {
121
+ type: "number",
122
+ description:
123
+ "1-based inclusive line at which the page ends; use the returned value to page backward.",
124
+ },
125
+ lineCharacterOffset: {
126
+ type: "number",
127
+ description: "Character offset within each source line.",
128
+ },
129
+ maxLineCharacters: {
130
+ type: "number",
131
+ description:
132
+ "Characters shown per line (default 4000, maximum 10000).",
133
+ },
134
+ characterOffset: {
135
+ type: "number",
136
+ description: "Character offset within the rendered tail page.",
137
+ },
138
+ maxCharacters: {
139
+ type: "number",
140
+ description: "Response size (default 20000, hard maximum 50000).",
73
141
  },
74
142
  },
75
143
  },
@@ -80,6 +80,13 @@ export class LazyToolsService extends ToolsService {
80
80
  return this.tools; // Returns filtered subset
81
81
  }
82
82
 
83
+ /** Rebuild the lazy catalog while preserving enable/disable preferences. */
84
+ resetTools(): void {
85
+ this.allTools = [];
86
+ super.resetTools();
87
+ this.registerLazyTools();
88
+ }
89
+
83
90
  // Enable tools matching glob patterns
84
91
  enableTools(patterns: string[]) {
85
92
  for (const pattern of patterns) {
@@ -388,8 +388,15 @@ export class McpService {
388
388
  }
389
389
 
390
390
  async closeAll() {
391
- await this.closeTransports();
392
- await this.closeClients();
391
+ await Promise.allSettled([this.closeTransports()]);
392
+ await Promise.allSettled([this.closeClients()]);
393
+ // Connections are recreated from freshly-read configuration after reload.
394
+ this.clients = [];
395
+ this.transports = [];
396
+ this.connected = [];
397
+ this.config = [];
398
+ this.tools = [];
399
+ this.toolAliases = {};
393
400
  }
394
401
 
395
402
  getClientIndex(clientName: string) {
@@ -0,0 +1,31 @@
1
+ export interface RuntimeReloadResult {
2
+ tools: number;
3
+ mcps: number;
4
+ modules: number;
5
+ }
6
+
7
+ /** Coordinates in-place runtime refreshes and coalesces concurrent requests. */
8
+ export class RuntimeReloadService {
9
+ private handler?: () => Promise<RuntimeReloadResult>;
10
+ private pending?: Promise<RuntimeReloadResult>;
11
+
12
+ configure(handler: () => Promise<RuntimeReloadResult>): void {
13
+ this.handler = handler;
14
+ }
15
+
16
+ isConfigured(): boolean {
17
+ return !!this.handler;
18
+ }
19
+
20
+ reload(): Promise<RuntimeReloadResult> {
21
+ if (!this.handler) {
22
+ return Promise.reject(new Error("Runtime reload is unavailable for this command"));
23
+ }
24
+ if (!this.pending) {
25
+ this.pending = this.handler().finally(() => {
26
+ this.pending = undefined;
27
+ });
28
+ }
29
+ return this.pending;
30
+ }
31
+ }
@@ -79,6 +79,17 @@ export class ToolsService {
79
79
  }
80
80
  }
81
81
 
82
+ /**
83
+ * Remove every registered tool and handler while retaining service context,
84
+ * wrappers, and overrides. Runtime reload uses this before rebuilding the
85
+ * catalog from built-ins, MCP servers, and configured modules.
86
+ */
87
+ resetTools(): void {
88
+ this.tools = [];
89
+ this.functions = {};
90
+ this.originalFunctions = {};
91
+ }
92
+
82
93
  getContext(): ToolContext {
83
94
  return this.context;
84
95
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * TracingService — a zero-dependency, no-op-by-default tracing abstraction
3
+ * for the @tyvm/knowhow core package.
4
+ *
5
+ * The core package ships with NO OpenTelemetry dependencies. This service
6
+ * defines the interface that the decorators in `src/util/Trace.ts` call into,
7
+ * and holds a singleton "current tracer" that starts as a no-op.
8
+ *
9
+ * A module (e.g. @tyvm/knowhow-module-tracing) can call
10
+ * `TracingService.register(impl)` during its `init()` to swap in a real OTEL
11
+ * implementation. After that, every @Trace / @TraceAll / tracify call in the
12
+ * knowhow codebase will produce real spans — with zero changes to the core.
13
+ */
14
+
15
+ export interface SpanHandle {
16
+ /** Set a string/number/boolean attribute on the span. */
17
+ setAttribute(key: string, value: string | number | boolean): void;
18
+ /** Mark the span as errored and record the exception. */
19
+ recordError(err: unknown): void;
20
+ /** End the span (records wall-clock end time). */
21
+ end(): void;
22
+ }
23
+
24
+ export interface TracerImpl {
25
+ /**
26
+ * Start a span. The implementation is responsible for making it a child of
27
+ * the currently-active span (via OpenTelemetry context propagation or
28
+ * equivalent).
29
+ *
30
+ * @param name Span name, e.g. "ClassName.methodName"
31
+ * @param attributes Initial attributes to attach
32
+ */
33
+ startSpan(
34
+ name: string,
35
+ attributes?: Record<string, string | number | boolean>
36
+ ): SpanHandle;
37
+
38
+ /**
39
+ * Run `fn` with the given span active so that child spans created inside it
40
+ * are automatically parented to this one.
41
+ *
42
+ * If context propagation is not supported, implementations can simply call
43
+ * `fn()` directly.
44
+ */
45
+ withSpan<T>(span: SpanHandle, fn: () => T): T;
46
+ }
47
+
48
+ // ─── No-op implementation used until a real tracer is registered ─────────────
49
+
50
+ const noopSpan: SpanHandle = {
51
+ setAttribute() {},
52
+ recordError() {},
53
+ end() {},
54
+ };
55
+
56
+ const noopTracer: TracerImpl = {
57
+ startSpan() {
58
+ return noopSpan;
59
+ },
60
+ withSpan(_span, fn) {
61
+ return fn();
62
+ },
63
+ };
64
+
65
+ // ─── Singleton state ──────────────────────────────────────────────────────────
66
+
67
+ let _impl: TracerImpl = noopTracer;
68
+
69
+ /**
70
+ * TracingService — access point for the current tracer implementation.
71
+ *
72
+ * Usage in decorators / instrumentation code:
73
+ * ```ts
74
+ * import { TracingService } from '../services/TracingService';
75
+ * const span = TracingService.startSpan('MyClass.myMethod');
76
+ * ```
77
+ *
78
+ * Usage in a module to plug in a real OTEL tracer:
79
+ * ```ts
80
+ * import { TracingService } from '@tyvm/knowhow/...';
81
+ * TracingService.register(myOtelTracerImpl);
82
+ * ```
83
+ */
84
+ export const TracingService = {
85
+ /**
86
+ * Register a real tracer implementation. Called by the tracing module during
87
+ * `init()`. The registration is global and immediate — all subsequent
88
+ * decorator/tracify calls will use this implementation.
89
+ */
90
+ register(impl: TracerImpl): void {
91
+ _impl = impl;
92
+ },
93
+
94
+ /** Reset to the no-op tracer (useful for tests). */
95
+ reset(): void {
96
+ _impl = noopTracer;
97
+ },
98
+
99
+ /** Whether a real (non-no-op) tracer has been registered. */
100
+ isEnabled(): boolean {
101
+ return _impl !== noopTracer;
102
+ },
103
+
104
+ /** Start a span using the current implementation. */
105
+ startSpan(
106
+ name: string,
107
+ attributes?: Record<string, string | number | boolean>
108
+ ): SpanHandle {
109
+ return _impl.startSpan(name, attributes);
110
+ },
111
+
112
+ /** Run a function with a span active (context propagation). */
113
+ withSpan<T>(span: SpanHandle, fn: () => T): T {
114
+ return _impl.withSpan(span, fn);
115
+ },
116
+ };
@@ -15,8 +15,10 @@ import { SessionManager } from "./SessionManager";
15
15
  import { TaskRegistry } from "./TaskRegistry";
16
16
  import { MediaProcessorService } from "./MediaProcessorService";
17
17
  import { BehaviorsService } from "./BehaviorsService";
18
+ import { RuntimeReloadService } from "./RuntimeReloadService";
18
19
 
19
20
  import { ConversionService } from "./conversion/ConversionService";
21
+ import { TracingService } from "./TracingService";
20
22
 
21
23
  export * from "./AgentService";
22
24
  export * from "./EventService";
@@ -38,7 +40,11 @@ export * from "./SyncedAgentWatcher";
38
40
  export * from "./SyncerService";
39
41
  export * from "./watchers";
40
42
  export { Clients } from "../clients";
43
+ export * from "../util/Trace";
44
+ export { TracingService } from "./TracingService";
45
+ export type { TracerImpl, SpanHandle } from "./TracingService";
41
46
  export * from "./conversion";
47
+ export * from "./RuntimeReloadService";
42
48
  export { BehaviorsService } from "./BehaviorsService";
43
49
 
44
50
  let Singletons = {} as {
@@ -56,6 +62,7 @@ let Singletons = {} as {
56
62
  MediaProcessor: MediaProcessorService;
57
63
  Conversion: ConversionService;
58
64
  Behaviors: BehaviorsService;
65
+ RuntimeReload: RuntimeReloadService;
59
66
  };
60
67
 
61
68
  export const services = (): typeof Singletons => {
@@ -88,6 +95,7 @@ export const services = (): typeof Singletons => {
88
95
  Tools,
89
96
  knowhowApiClient: new KnowhowSimpleClient(),
90
97
  Behaviors,
98
+ RuntimeReload: new RuntimeReloadService(),
91
99
  };
92
100
 
93
101
  Singletons.Tools.setContext({
@@ -39,6 +39,13 @@ function globalResolvePaths(): string[] {
39
39
  }
40
40
 
41
41
  export class ModulesService {
42
+ private _loadedModules: {
43
+ modulePath: string;
44
+ resolvedPath: string;
45
+ module: KnowhowModule;
46
+ params: any;
47
+ }[] = [];
48
+
42
49
  async getDefaultContext() {
43
50
  return { ...services() };
44
51
  }
@@ -87,6 +94,7 @@ export class ModulesService {
87
94
  // best driver during phase 2.
88
95
  const loadedModules: {
89
96
  modulePath: string;
97
+ resolvedPath: string;
90
98
  module: KnowhowModule;
91
99
  }[] = [];
92
100
 
@@ -128,6 +136,7 @@ export class ModulesService {
128
136
  // global install).
129
137
  let importedModule: KnowhowModule;
130
138
  let loaded = false;
139
+ let selectedResolvedPath = modulePath;
131
140
  const errors: { candidate: string; error: Error }[] = [];
132
141
  for (const resolvedPath of candidates) {
133
142
  try {
@@ -147,6 +156,7 @@ export class ModulesService {
147
156
  context: context as ModuleContext,
148
157
  });
149
158
  }
159
+ selectedResolvedPath = resolvedPath;
150
160
  loaded = true;
151
161
  break;
152
162
  } catch (err: any) {
@@ -168,12 +178,17 @@ export class ModulesService {
168
178
  );
169
179
  continue;
170
180
  }
171
- loadedModules.push({ modulePath, module: importedModule });
181
+ loadedModules.push({
182
+ modulePath,
183
+ resolvedPath: selectedResolvedPath,
184
+ module: importedModule,
185
+ });
172
186
  }
173
187
 
174
188
  // Phase 2: init every successfully-loaded module now that all `register`
175
189
  // phases have run and injected their services into `context`.
176
- for (const { modulePath, module: importedModule } of loadedModules) {
190
+ for (const { modulePath, resolvedPath, module: importedModule } of loadedModules) {
191
+ const initParams = { config, cwd: process.cwd(), context: context as ModuleContext };
177
192
  try {
178
193
  await importedModule.init({
179
194
  config,
@@ -190,6 +205,8 @@ export class ModulesService {
190
205
  );
191
206
  continue;
192
207
  }
208
+ this._loadedModules.push({ modulePath, resolvedPath, module: importedModule, params: initParams });
209
+
193
210
  // Only register tools/agents/plugins/clients if the relevant services
194
211
  // are available in context (they may not be during early CLI command registration)
195
212
  if (context.Agents) {
@@ -226,6 +243,32 @@ export class ModulesService {
226
243
  }
227
244
  }
228
245
 
246
+ /**
247
+ * Call `destroy()` on every successfully-initialized module, in reverse
248
+ * initialization order (last-in, first-out). Errors are caught and logged
249
+ * so one broken module does not prevent others from cleaning up.
250
+ */
251
+ async destroyModules() {
252
+ const toDestroy = [...this._loadedModules].reverse();
253
+ // Clear first so repeated shutdown/reload attempts cannot destroy twice.
254
+ this._loadedModules = [];
255
+ for (const { modulePath, resolvedPath, module: mod, params } of toDestroy) {
256
+ if (typeof mod.destroy === "function") {
257
+ try {
258
+ await mod.destroy(params);
259
+ } catch (err: any) {
260
+ process.stderr.write(
261
+ `\n⚠️ Error in module destroy "${modulePath}": ${err?.message || err}\n\n`
262
+ );
263
+ }
264
+ }
265
+ // Re-require the entry point so source/configuration changes are visible.
266
+ if (require.cache[resolvedPath]) {
267
+ delete require.cache[resolvedPath];
268
+ }
269
+ }
270
+ }
271
+
229
272
  async loadModulesFromConfig(context?: ModuleContext) {
230
273
  const config = await getConfig();
231
274
 
@@ -16,6 +16,7 @@ import { ConversionService } from "../conversion/ConversionService";
16
16
  import { BehaviorsService } from "../BehaviorsService";
17
17
  import { ComputerUseService } from "./computerUse";
18
18
 
19
+ import { TracingService as TracingServiceType } from "../TracingService";
19
20
  /*
20
21
  *
21
22
  * A a module should allow the dynamic composition of npm modules that are installed globally by referencing an array of config
@@ -66,6 +67,7 @@ export interface ModuleContext {
66
67
  Behaviors?: BehaviorsService;
67
68
  ComputerUse?: ComputerUseService;
68
69
  [key: string]: any;
70
+ Tracing?: typeof TracingServiceType;
69
71
  }
70
72
 
71
73
  export interface KnowhowModule {
@@ -89,6 +91,12 @@ export interface KnowhowModule {
89
91
  * services registered by other modules' `register` phase, etc.).
90
92
  */
91
93
  init: (params: InitParams) => Promise<void>;
94
+ /**
95
+ * Phase 3 (optional): called when the CLI is shutting down. Use this to
96
+ * cleanly release resources — close connections, flush buffers, stop
97
+ * watchers, etc. Mirror of `init`.
98
+ */
99
+ destroy?: (params: InitParams) => Promise<void>;
92
100
  commands: ModuleChatCommand[];
93
101
  tools: ModuleTool[];
94
102
  agents: ModuleAgent[];