@tyvm/knowhow 0.0.138 → 0.0.141

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 (173) hide show
  1. package/autodoc/chat-guide.md +1 -0
  2. package/package.json +5 -2
  3. package/scripts/build-for-node.sh +4 -4
  4. package/src/agents/base/base.ts +254 -29
  5. package/src/agents/patcher/patcher.ts +1 -1
  6. package/src/agents/researcher/researcher.ts +2 -2
  7. package/src/agents/tools/agentCall.ts +6 -0
  8. package/src/agents/tools/connectAgent.ts +353 -0
  9. package/src/agents/tools/index.ts +6 -0
  10. package/src/agents/tools/list.ts +48 -0
  11. package/src/agents/tools/observe.ts +245 -0
  12. package/src/agents/tools/replyToParent.ts +122 -0
  13. package/src/agents/tools/runGenerate.ts +209 -0
  14. package/src/agents/tools/sendAgentMessage.ts +138 -0
  15. package/src/agents/tools/startAgentTask.ts +92 -5
  16. package/src/agents/tools/waitForAgentCompleted.ts +269 -0
  17. package/src/ai.ts +1 -0
  18. package/src/chat/modules/AgentModule.ts +89 -27
  19. package/src/chat/modules/SessionsModule.ts +6 -10
  20. package/src/clients/contextLimits.ts +26 -0
  21. package/src/clients/pricing/anthropic.ts +12 -0
  22. package/src/commands/agent.ts +22 -0
  23. package/src/commands/agents.ts +83 -0
  24. package/src/commands/misc.ts +6 -0
  25. package/src/commands/modules.ts +159 -7
  26. package/src/commands/services.ts +14 -0
  27. package/src/generate.ts +25 -3
  28. package/src/services/AgentService.ts +21 -3
  29. package/src/services/AgentSyncFs.ts +56 -0
  30. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  31. package/src/services/EventService.ts +4 -1
  32. package/src/services/KnowhowClient.ts +7 -0
  33. package/src/services/LazyToolsService.ts +7 -3
  34. package/src/services/MinimalToolsService.ts +15 -7
  35. package/src/services/SyncedAgentWatcher.ts +2 -0
  36. package/src/services/SyncerService.ts +35 -6
  37. package/src/services/Tools.ts +43 -3
  38. package/src/services/modules/index.ts +127 -46
  39. package/src/types.ts +5 -0
  40. package/tests/agents/interrupt.test.ts +11 -18
  41. package/tests/agents/observe-base.test.ts +183 -0
  42. package/tests/agents/tools/connectAgent.test.ts +138 -0
  43. package/tests/agents/tools/observe.test.ts +179 -0
  44. package/tests/agents/tools/replyToParent.test.ts +125 -0
  45. package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
  46. package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
  47. package/tests/chat/detach-attach-rendering.test.ts +315 -0
  48. package/tests/clients/contextLimits.test.ts +23 -1
  49. package/tests/clients/pricing.test.ts +20 -1
  50. package/tests/services/Tools.test.ts +172 -9
  51. package/tests/unit/modules/moduleLoading.test.ts +69 -0
  52. package/ts_build/package.json +5 -2
  53. package/ts_build/src/agents/base/base.d.ts +24 -1
  54. package/ts_build/src/agents/base/base.js +111 -16
  55. package/ts_build/src/agents/base/base.js.map +1 -1
  56. package/ts_build/src/agents/patcher/patcher.js +1 -1
  57. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  58. package/ts_build/src/agents/researcher/researcher.js +2 -2
  59. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  60. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  61. package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
  62. package/ts_build/src/agents/tools/connectAgent.js +284 -0
  63. package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
  64. package/ts_build/src/agents/tools/index.d.ts +6 -0
  65. package/ts_build/src/agents/tools/index.js +6 -0
  66. package/ts_build/src/agents/tools/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/list.js +40 -0
  68. package/ts_build/src/agents/tools/list.js.map +1 -1
  69. package/ts_build/src/agents/tools/observe.d.ts +20 -0
  70. package/ts_build/src/agents/tools/observe.js +144 -0
  71. package/ts_build/src/agents/tools/observe.js.map +1 -0
  72. package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
  73. package/ts_build/src/agents/tools/replyToParent.js +118 -0
  74. package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
  75. package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
  76. package/ts_build/src/agents/tools/runGenerate.js +138 -0
  77. package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
  78. package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
  79. package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
  80. package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
  81. package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
  82. package/ts_build/src/agents/tools/startAgentTask.js +53 -6
  83. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  84. package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
  85. package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
  86. package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
  87. package/ts_build/src/ai.d.ts +1 -0
  88. package/ts_build/src/ai.js.map +1 -1
  89. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
  90. package/ts_build/src/chat/modules/AgentModule.js +46 -21
  91. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/SessionsModule.js +1 -6
  93. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  94. package/ts_build/src/clients/contextLimits.js +21 -0
  95. package/ts_build/src/clients/contextLimits.js.map +1 -1
  96. package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
  97. package/ts_build/src/clients/pricing/anthropic.js +9 -0
  98. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  99. package/ts_build/src/commands/agent.js +11 -0
  100. package/ts_build/src/commands/agent.js.map +1 -1
  101. package/ts_build/src/commands/agents.js +61 -0
  102. package/ts_build/src/commands/agents.js.map +1 -1
  103. package/ts_build/src/commands/misc.js +2 -0
  104. package/ts_build/src/commands/misc.js.map +1 -1
  105. package/ts_build/src/commands/modules.js +86 -7
  106. package/ts_build/src/commands/modules.js.map +1 -1
  107. package/ts_build/src/commands/services.js +10 -0
  108. package/ts_build/src/commands/services.js.map +1 -1
  109. package/ts_build/src/generate.d.ts +2 -0
  110. package/ts_build/src/generate.js +11 -3
  111. package/ts_build/src/generate.js.map +1 -1
  112. package/ts_build/src/services/AgentService.d.ts +4 -1
  113. package/ts_build/src/services/AgentService.js +9 -2
  114. package/ts_build/src/services/AgentService.js.map +1 -1
  115. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  116. package/ts_build/src/services/AgentSyncFs.js +35 -0
  117. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
  119. package/ts_build/src/services/EventService.d.ts +4 -1
  120. package/ts_build/src/services/EventService.js.map +1 -1
  121. package/ts_build/src/services/KnowhowClient.js +3 -0
  122. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  123. package/ts_build/src/services/LazyToolsService.d.ts +2 -2
  124. package/ts_build/src/services/LazyToolsService.js +2 -2
  125. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  126. package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
  127. package/ts_build/src/services/MinimalToolsService.js +6 -6
  128. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  129. package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
  130. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  131. package/ts_build/src/services/SyncerService.d.ts +2 -1
  132. package/ts_build/src/services/SyncerService.js +8 -2
  133. package/ts_build/src/services/SyncerService.js.map +1 -1
  134. package/ts_build/src/services/Tools.d.ts +6 -1
  135. package/ts_build/src/services/Tools.js +9 -3
  136. package/ts_build/src/services/Tools.js.map +1 -1
  137. package/ts_build/src/services/modules/index.d.ts +1 -1
  138. package/ts_build/src/services/modules/index.js +67 -27
  139. package/ts_build/src/services/modules/index.js.map +1 -1
  140. package/ts_build/src/types.d.ts +3 -0
  141. package/ts_build/src/types.js.map +1 -1
  142. package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
  143. package/ts_build/tests/agents/interrupt.test.js +3 -13
  144. package/ts_build/tests/agents/interrupt.test.js.map +1 -1
  145. package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
  146. package/ts_build/tests/agents/observe-base.test.js +149 -0
  147. package/ts_build/tests/agents/observe-base.test.js.map +1 -0
  148. package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
  149. package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
  150. package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
  151. package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
  152. package/ts_build/tests/agents/tools/observe.test.js +148 -0
  153. package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
  154. package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
  155. package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
  156. package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
  157. package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
  158. package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
  159. package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
  160. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
  161. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
  162. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
  163. package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
  164. package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
  165. package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
  166. package/ts_build/tests/clients/contextLimits.test.js +11 -1
  167. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  168. package/ts_build/tests/clients/pricing.test.js +10 -1
  169. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  170. package/ts_build/tests/services/Tools.test.js +126 -8
  171. package/ts_build/tests/services/Tools.test.js.map +1 -1
  172. package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
  173. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
@@ -1,4 +1,4 @@
1
- import { ToolsService, ToolContext } from "./Tools";
1
+ import { ToolsService, ToolContext, ToolCallContext } from "./Tools";
2
2
  import { Tool, ToolCall } from "../clients/types";
3
3
  import { callTool } from "../agents/tools/minimal/callTool";
4
4
  import { inspectTools } from "../agents/tools/minimal/inspectTools";
@@ -107,7 +107,11 @@ export class MinimalToolsService extends ToolsService {
107
107
  * Used both by direct calls (agent calls the tool by its real name) and by
108
108
  * the callTool meta-tool (agent calls callTool({ name, args })).
109
109
  */
110
- private async dispatchToTool(toolCall: ToolCall, resolvedName: string) {
110
+ private async dispatchToTool(
111
+ toolCall: ToolCall,
112
+ resolvedName: string,
113
+ callContext?: ToolCallContext
114
+ ) {
111
115
  // Temporarily surface the target tool in the visible list so base
112
116
  // ToolsService.callTool can find its definition, then restore state.
113
117
  const alreadyVisible = super
@@ -123,7 +127,7 @@ export class MinimalToolsService extends ToolsService {
123
127
  }
124
128
  }
125
129
 
126
- const result = await super.callTool(toolCall, [resolvedName]);
130
+ const result = await super.callTool(toolCall, [resolvedName], callContext);
127
131
 
128
132
  // Remove the temporarily-added tool to keep the visible set stable
129
133
  if (!alreadyVisible) {
@@ -136,7 +140,11 @@ export class MinimalToolsService extends ToolsService {
136
140
  /**
137
141
  * Delegates to the full catalog for resolution, then runs via callToolByName.
138
142
  */
139
- async callTool(toolCall: ToolCall, enabledTools?: string[]): Promise<any> {
143
+ async callTool(
144
+ toolCall: ToolCall,
145
+ enabledTools?: string[],
146
+ callContext?: ToolCallContext
147
+ ): Promise<any> {
140
148
  const functionName = toolCall.function.name;
141
149
 
142
150
  // Base tools (callTool, inspectTools, finalAnswer) → standard dispatch
@@ -145,7 +153,7 @@ export class MinimalToolsService extends ToolsService {
145
153
  .some((t) => t.function.name === functionName);
146
154
 
147
155
  if (isBaseTool) {
148
- return super.callTool(toolCall, this.getToolNames());
156
+ return super.callTool(toolCall, this.getToolNames(), callContext);
149
157
  }
150
158
 
151
159
  // Extended tools: called directly by name (not via the callTool meta-tool).
@@ -158,10 +166,10 @@ export class MinimalToolsService extends ToolsService {
158
166
  );
159
167
 
160
168
  if (existsInCatalog) {
161
- return this.dispatchToTool(toolCall, functionName);
169
+ return this.dispatchToTool(toolCall, functionName, callContext);
162
170
  }
163
171
 
164
172
  // Not found anywhere - let the base class produce the standard error.
165
- return super.callTool(toolCall, [functionName]);
173
+ return super.callTool(toolCall, [functionName], callContext);
166
174
  }
167
175
  }
@@ -58,6 +58,8 @@ export interface AttachableAgent {
58
58
  unpause(): void | Promise<void>;
59
59
  kill(): void | Promise<void>;
60
60
  interrupt(message?: string): void | Promise<void>;
61
+ /** Force conversation compaction at the next loop iteration (local agents only). */
62
+ requestCompact?(): void;
61
63
  addPendingUserMessage(message: Message): void;
62
64
  }
63
65
 
@@ -13,10 +13,16 @@ export interface SyncerOptions {
13
13
  messageId?: string;
14
14
  /** Force fs sync even if messageId is provided */
15
15
  syncFs?: boolean;
16
- /** Use an existing Knowhow task ID instead of creating a new one */
17
- existingKnowhowTaskId?: string;
16
+ /**
17
+ * Push the agent's work to a remote Knowhow task. When set (and there is no
18
+ * messageId), the `taskId` is treated as the remote task ID to attach to and
19
+ * push updates to, rather than creating a new remote task.
20
+ */
21
+ syncRemote?: boolean;
18
22
  /** Agent name to persist in metadata.json */
19
23
  agentName?: string;
24
+ /** The taskId of the parent agent that spawned this task, if any. */
25
+ parentTaskId?: string;
20
26
  }
21
27
 
22
28
  export interface AgentSyncer {
@@ -37,7 +43,8 @@ export interface AgentSyncer {
37
43
  *
38
44
  * Decision logic:
39
45
  * - Always sets up AgentSyncFs (primary local syncer)
40
- * - If messageId is present AND syncFs is not forced AND no existingKnowhowTaskId → also sets up AgentSyncKnowhowWeb
46
+ * - If messageId is present AND syncFs is not forced → creates a new remote task via AgentSyncKnowhowWeb
47
+ * - If syncRemote is set (and no messageId) → attaches to the remote task identified by taskId
41
48
  */
42
49
  export class SyncerService implements AgentSyncer {
43
50
  private fsSync: AgentSyncFs;
@@ -59,11 +66,23 @@ export class SyncerService implements AgentSyncer {
59
66
  this.active = true;
60
67
  this.useWebSync = false;
61
68
 
62
- // Determine whether to use web sync
69
+ // Determine whether to CREATE a new remote (web) task: only when we have a
70
+ // messageId and aren't forcing fs-only. A messageId means "definitely
71
+ // remote" — create a fresh remote task from that message.
63
72
  const shouldUseWebSync =
64
73
  !!options.messageId &&
65
- !options.syncFs &&
66
- !options.existingKnowhowTaskId;
74
+ !options.syncFs;
75
+
76
+ // Determine whether to ATTACH to an already-existing remote task and push
77
+ // local work to it (no creation needed). This is the "--sync-remote" path:
78
+ // the caller wants to push work to the remote task identified by taskId.
79
+ // We only auto-attach when there is NO messageId (messageId always creates
80
+ // a fresh remote task above).
81
+ //
82
+ // This is orthogonal to fs sync: we can push to the remote task AND keep
83
+ // the local .knowhow/processes/agents/<id>/ files in sync at the same time.
84
+ const shouldAttachExisting =
85
+ !!options.syncRemote && !options.messageId && !!options.taskId;
67
86
 
68
87
  // Always create fs sync task first
69
88
  console.log(
@@ -73,6 +92,7 @@ export class SyncerService implements AgentSyncer {
73
92
  taskId: options.taskId,
74
93
  prompt: options.prompt,
75
94
  agentName: options.agentName,
95
+ parentTaskId: options.parentTaskId,
76
96
  });
77
97
 
78
98
  // Optionally create web sync task
@@ -89,6 +109,15 @@ export class SyncerService implements AgentSyncer {
89
109
  }
90
110
  }
91
111
 
112
+ // Attach live web sync to an existing remote task (push updates to it).
113
+ if (!shouldUseWebSync && shouldAttachExisting) {
114
+ this.useWebSync = true;
115
+ this.createdTaskId = options.taskId;
116
+ console.log(
117
+ `🌐 Attaching web sync to existing remote task: ${options.taskId}`
118
+ );
119
+ }
120
+
92
121
  return fsTaskId;
93
122
  }
94
123
 
@@ -16,6 +16,28 @@ import { PluginService } from "../plugins/plugins";
16
16
  import { McpService } from "./Mcp";
17
17
  import { BehaviorsService } from "./BehaviorsService";
18
18
 
19
+ /**
20
+ * Per-invocation context passed into a tool call as `_ctx`.
21
+ *
22
+ * This is threaded through `callTool` and delivered to the tool implementation
23
+ * as the LAST positional argument (for positional tools) or merged onto the
24
+ * args object under the `_ctx` key (for non-positional tools).
25
+ *
26
+ * It enables self-referential tools: the calling agent can pass itself in via
27
+ * `caller`, so tools can read `caller.getCurrentTaskId()`, call
28
+ * `caller.addPendingUserMessage(...)`, subscribe to its events, etc. — without
29
+ * relying on shared mutable state on the ToolsService (which would break under
30
+ * concurrent tool usage).
31
+ */
32
+ export interface ToolCallContext {
33
+ /** The agent (or other entity) that initiated this tool call. */
34
+ caller?: any;
35
+ /** The task id of the calling agent, if known. */
36
+ taskId?: string;
37
+ /** Arbitrary additional per-call context. */
38
+ [key: string]: any;
39
+ }
40
+
19
41
  export interface ToolContext {
20
42
  Agents?: AgentService;
21
43
  Events?: EventService;
@@ -174,7 +196,11 @@ export class ToolsService {
174
196
  this.addFunctions(functions);
175
197
  }
176
198
 
177
- async callTool(toolCall: ToolCall, enabledTools = this.getToolNames()) {
199
+ async callTool(
200
+ toolCall: ToolCall,
201
+ enabledTools = this.getToolNames(),
202
+ callContext?: ToolCallContext
203
+ ) {
178
204
  const functionName = toolCall.function.name;
179
205
  let functionArgs: any;
180
206
 
@@ -219,10 +245,21 @@ export class ToolsService {
219
245
  const properties = toolDefinition?.function?.parameters?.properties || {};
220
246
  const isPositional =
221
247
  toolDefinition?.function?.parameters?.positional || false;
222
- const fnArgs = isPositional
248
+
249
+ // Thread the per-call context (`_ctx`) into the invocation so tools can be
250
+ // self-referential (e.g. read the calling agent off `_ctx.caller`).
251
+ // - positional tools receive `_ctx` as the LAST positional argument
252
+ // - non-positional tools receive it merged onto the args object as `_ctx`
253
+ const ctx: ToolCallContext = { ...(callContext || {}), Tools: this };
254
+
255
+ const positionalArgs = isPositional
223
256
  ? Object.keys(properties).map((p) => functionArgs[p])
224
257
  : functionArgs;
225
258
 
259
+ const fnArgs = isPositional
260
+ ? [...positionalArgs, ctx]
261
+ : { ...functionArgs, _ctx: ctx };
262
+
226
263
  // Execute the function
227
264
  const rawResponse = isPositional
228
265
  ? functionToCall.call(this, ...fnArgs)
@@ -250,7 +287,10 @@ export class ToolsService {
250
287
  // Handle special case for parallel tool use
251
288
  if (functionName === "multi_tool_use.parallel") {
252
289
  // Extract tool_calls array from arguments
253
- const toolCallsArg = Array.isArray(fnArgs) ? fnArgs : [fnArgs];
290
+ // Use the original (un-`_ctx`-augmented) args for extraction.
291
+ const toolCallsArg = Array.isArray(functionArgs)
292
+ ? functionArgs
293
+ : [functionArgs];
254
294
  const toolCalls = Array.isArray(toolCallsArg) ? toolCallsArg : [];
255
295
 
256
296
  const calls = toolCalls as {
@@ -7,6 +7,37 @@ import { KnowhowModule, ModuleContext } from "./types";
7
7
  import { services } from "../";
8
8
  import { toUniqueArray } from "../../utils";
9
9
 
10
+ /**
11
+ * Module resolution search paths.
12
+ *
13
+ * A module's resolution order depends on WHICH config declared it:
14
+ *
15
+ * - Modules declared in the GLOBAL config should resolve against the global
16
+ * install's node_modules (~/.knowhow/node_modules) — i.e. what's installed
17
+ * alongside the global config. We still fall back to cwd paths as a last
18
+ * resort so a globally-declared module can still be found if it only exists
19
+ * locally.
20
+ *
21
+ * - Modules declared in the LOCAL config should resolve against the local
22
+ * project first (cwd/.knowhow/node_modules), then the global config's
23
+ * node_modules (~/.knowhow/node_modules), then cwd/node_modules.
24
+ */
25
+ function localResolvePaths(): string[] {
26
+ return [
27
+ path.join(process.cwd(), ".knowhow", "node_modules"),
28
+ path.join(os.homedir(), ".knowhow", "node_modules"),
29
+ path.join(process.cwd(), "node_modules"),
30
+ ];
31
+ }
32
+
33
+ function globalResolvePaths(): string[] {
34
+ return [
35
+ path.join(os.homedir(), ".knowhow", "node_modules"),
36
+ path.join(process.cwd(), ".knowhow", "node_modules"),
37
+ path.join(process.cwd(), "node_modules"),
38
+ ];
39
+ }
40
+
10
41
  export class ModulesService {
11
42
  async getDefaultContext() {
12
43
  return { ...services() };
@@ -19,7 +50,8 @@ export class ModulesService {
19
50
 
20
51
  async loadModulesFrom(
21
52
  config: { modules: string[] } & any,
22
- context?: Partial<ModuleContext>
53
+ context?: Partial<ModuleContext>,
54
+ resolvePaths: string[] = localResolvePaths()
23
55
  ) {
24
56
  // If no context provided, fall back to global singletons
25
57
  if (!context) {
@@ -38,54 +70,84 @@ export class ModulesService {
38
70
 
39
71
  const allModulePaths = config.modules;
40
72
 
41
- // Search paths: local .knowhow/node_modules first (where `knowhow modules install`
42
- // puts packages for this project), then global ~/.knowhow/node_modules, and finally
43
- // cwd/node_modules as a last resort. Putting cwd/node_modules last avoids accidentally
44
- // picking up workspace-symlinked dev versions of modules (e.g. when running knowhow
45
- // from within the knowhow monorepo itself) instead of the properly-installed version.
46
- const resolvePaths = [
47
- path.join(process.cwd(), ".knowhow", "node_modules"),
48
- path.join(os.homedir(), ".knowhow", "node_modules"),
49
- path.join(process.cwd(), "node_modules"),
50
- ];
73
+ // `resolvePaths` (passed in by the caller) determines the search-path order.
74
+ // Global-config modules resolve against ~/.knowhow/node_modules first; local-config
75
+ // modules resolve against cwd/.knowhow/node_modules first. See localResolvePaths()
76
+ // and globalResolvePaths() above.
51
77
 
52
78
  for (const modulePath of allModulePaths) {
53
- // Resolve relative paths relative to process.cwd() so that paths like
54
- // "../../packages/knowhow-module-load-webpage" in knowhow.json work
55
- // regardless of where the compiled output lives.
56
- let resolvedPath: string;
79
+ // Build an ordered list of candidate resolutions for this module.
80
+ // Relative paths resolve to a single candidate (relative to cwd), while
81
+ // npm package names get one candidate per search path so we can fall
82
+ // back to the global install if a locally-symlinked (e.g. workspace dev)
83
+ // build fails to load (wrong Node ABI, broken build, etc.).
84
+ const candidates: string[] = [];
57
85
  if (modulePath.startsWith(".")) {
58
- resolvedPath = path.resolve(process.cwd(), modulePath);
86
+ candidates.push(path.resolve(process.cwd(), modulePath));
59
87
  } else {
60
- // For npm package names, try resolving from cwd first so locally-installed
61
- // modules are found even when knowhow is installed globally.
88
+ // For npm package names, resolve against each search path individually
89
+ // (in priority order) so each becomes its own fallback candidate.
90
+ for (const searchPath of resolvePaths) {
91
+ try {
92
+ const resolved = require.resolve(modulePath, {
93
+ paths: [searchPath],
94
+ });
95
+ if (!candidates.includes(resolved)) candidates.push(resolved);
96
+ } catch {
97
+ // this search path doesn't have the module — try the next one
98
+ }
99
+ }
100
+ // Finally, normal require resolution as a last resort.
62
101
  try {
63
- resolvedPath = require.resolve(modulePath, { paths: resolvePaths });
102
+ const resolved = require.resolve(modulePath);
103
+ if (!candidates.includes(resolved)) candidates.push(resolved);
64
104
  } catch {
65
- resolvedPath = modulePath; // fall back to normal require resolution
105
+ // ignore — will fall through to the bare name below
66
106
  }
107
+ if (candidates.length === 0) candidates.push(modulePath);
67
108
  }
68
109
 
110
+ // Try each candidate in order. Only if EVERY candidate fails to load do
111
+ // we surface an error — a failure on one candidate (e.g. a stale local
112
+ // workspace build) should silently fall back to the next (e.g. the
113
+ // global install).
69
114
  let importedModule: KnowhowModule;
70
- try {
71
- const rawModule = require(resolvedPath);
72
- importedModule = (rawModule.default || rawModule) as KnowhowModule;
73
- context.Events?.log(
74
- "ModulesService",
75
- `🔌 Loading module: ${modulePath} (resolved: ${resolvedPath})`
76
- );
77
- await importedModule.init({
78
- config,
79
- cwd: process.cwd(),
80
- context: context as ModuleContext,
81
- });
82
- context.Events?.log(
83
- "ModulesService",
84
- `✅ Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
85
- );
86
- } catch (err: any) {
115
+ let loaded = false;
116
+ const errors: { candidate: string; error: Error }[] = [];
117
+ for (const resolvedPath of candidates) {
118
+ try {
119
+ const rawModule = require(resolvedPath);
120
+ importedModule = (rawModule.default || rawModule) as KnowhowModule;
121
+ context.Events?.log(
122
+ "ModulesService",
123
+ `🔌 Loading module: ${modulePath} (resolved: ${resolvedPath})`
124
+ );
125
+ await importedModule.init({
126
+ config,
127
+ cwd: process.cwd(),
128
+ context: context as ModuleContext,
129
+ });
130
+ context.Events?.log(
131
+ "ModulesService",
132
+ `✅ Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
133
+ );
134
+ loaded = true;
135
+ break;
136
+ } catch (err: any) {
137
+ errors.push({ candidate: resolvedPath, error: err });
138
+ // try the next candidate
139
+ }
140
+ }
141
+
142
+ if (!loaded) {
143
+ // All candidates failed — report the last (most fully-resolved) error.
144
+ const last = errors[errors.length - 1];
145
+ const detail = last
146
+ ? `${last.error.message}`
147
+ : "no resolvable module found";
87
148
  process.stderr.write(
88
- `\n⚠️ Failed to load module "${modulePath}": ${err.message}\n` +
149
+ `\n⚠️ Failed to load module "${modulePath}": ${detail}\n` +
150
+ ` Tried: ${candidates.join(", ") || "(none)"}\n` +
89
151
  ` Run "knowhow modules setup --global" or "knowhow modules install ${modulePath} --global" to fix this.\n\n`
90
152
  );
91
153
  continue;
@@ -130,14 +192,33 @@ export class ModulesService {
130
192
  const config = await getConfig();
131
193
 
132
194
  const globalConfig = await getGlobalConfig();
133
- const allModulePaths = [
134
- ...(globalConfig.modules || []),
135
- ...(config.modules || []),
136
- ];
137
-
138
- return this.loadModulesFrom(
139
- { ...config, modules: toUniqueArray(allModulePaths) },
140
- context
195
+
196
+ const globalModules = toUniqueArray(globalConfig.modules || []);
197
+ // Local modules already loaded globally shouldn't be loaded again (a module
198
+ // that adds a CLI command would throw "already have command"). De-dupe local
199
+ // against global.
200
+ const localModules = toUniqueArray(config.modules || []).filter(
201
+ (m) => !globalModules.includes(m)
141
202
  );
203
+
204
+ // Load global-config modules resolving against the global install's
205
+ // node_modules (~/.knowhow/node_modules) first.
206
+ if (globalModules.length > 0) {
207
+ await this.loadModulesFrom(
208
+ { ...config, modules: globalModules },
209
+ context,
210
+ globalResolvePaths()
211
+ );
212
+ }
213
+
214
+ // Load local-config modules resolving against the local project's
215
+ // node_modules (cwd/.knowhow/node_modules) first.
216
+ if (localModules.length > 0) {
217
+ await this.loadModulesFrom(
218
+ { ...config, modules: localModules },
219
+ context,
220
+ localResolvePaths()
221
+ );
222
+ }
142
223
  }
143
224
  }
package/src/types.ts CHANGED
@@ -35,6 +35,11 @@ export type GenerationSource = {
35
35
  outputName?: string;
36
36
  /** Pass to runAgentTask: enable .knowhow/processes/agents/<taskId> directory */
37
37
  syncFs?: boolean;
38
+ /**
39
+ * Pass to runAgentTask: push the agent's work to a remote Knowhow task
40
+ * (identified by this source's taskId).
41
+ */
42
+ syncRemote?: boolean;
38
43
  /**
39
44
  * Deterministic task ID for this source (used with syncFs).
40
45
  * Defaults to "generate:<sanitized-output>" when syncFs is enabled.
@@ -4,8 +4,10 @@
4
4
  * These verify the fix for the "agent gets stuck after /poke" bug:
5
5
  * 1. A stale (slow) operation that finishes AFTER its window was interrupted
6
6
  * must NOT clobber a newer interruptible window's resolver.
7
- * 2. An interrupt() arriving with no active window must be queued and fire on
8
- * the next makeInterruptible window (not silently dropped).
7
+ * 2. An interrupt() arriving with no active window is a no-op — there is
8
+ * nothing to interrupt, so it must NOT be queued (a queued interrupt would
9
+ * pre-empt the next tool call and drop its result). Any message supplied
10
+ * with the poke is still added as a pending user message.
9
11
  * 3. Normal (non-interrupted) resolution still works.
10
12
  * 4. Errors propagate when not interrupted, and are swallowed when interrupted.
11
13
  */
@@ -20,7 +22,6 @@
20
22
  class InterruptHarness {
21
23
  private _interruptResolve: (() => void) | null = null;
22
24
  private _interruptToken = 0;
23
- private _pendingInterrupt = false;
24
25
  public pendingUserMessages: any[] = [];
25
26
 
26
27
  addPendingUserMessage(m: any) {
@@ -43,12 +44,6 @@ class InterruptHarness {
43
44
 
44
45
  this._interruptResolve = () => finish(() => resolve(interruptValue));
45
46
 
46
- if (this._pendingInterrupt) {
47
- this._pendingInterrupt = false;
48
- finish(() => resolve(interruptValue));
49
- return;
50
- }
51
-
52
47
  promise
53
48
  .then((result) => {
54
49
  finish(() => resolve(result));
@@ -69,9 +64,8 @@ class InterruptHarness {
69
64
  }
70
65
  if (this._interruptResolve) {
71
66
  this._interruptResolve();
72
- } else {
73
- this._pendingInterrupt = true;
74
67
  }
68
+ // No active window -> nothing to interrupt. Do NOT queue it.
75
69
  }
76
70
 
77
71
  get hasActiveResolver() {
@@ -157,19 +151,18 @@ describe("BaseAgent interrupt / makeInterruptible", () => {
157
151
  await expect(w2).resolves.toBe("fresh-result");
158
152
  });
159
153
 
160
- test("interrupt() with no active window is queued and fires next window", async () => {
154
+ test("interrupt() with no active window is a no-op (NOT queued)", async () => {
161
155
  const h = new InterruptHarness();
162
156
  // No active window yet.
163
157
  expect(h.hasActiveResolver).toBe(false);
164
- h.interrupt(); // queued, not dropped
158
+ h.interrupt(); // nothing to interrupt -> no-op
165
159
 
166
- // Next window should fire immediately with the interrupt value.
160
+ // The next window must NOT be pre-empted — it should run its real op and
161
+ // resolve with the real result, not the interrupt value.
167
162
  const op = deferred<string>();
168
163
  const w = h.makeInterruptible(op.promise, "INT");
169
- await expect(w).resolves.toBe("INT");
170
- // Underlying op finishing later must be harmless.
171
- op.resolve("later");
172
- await op.promise;
164
+ op.resolve("real-result");
165
+ await expect(w).resolves.toBe("real-result");
173
166
  });
174
167
 
175
168
  test("errors propagate when not interrupted", async () => {