@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.
- package/autodoc/chat-guide.md +1 -0
- package/package.json +5 -2
- package/scripts/build-for-node.sh +4 -4
- package/src/agents/base/base.ts +254 -29
- package/src/agents/patcher/patcher.ts +1 -1
- package/src/agents/researcher/researcher.ts +2 -2
- package/src/agents/tools/agentCall.ts +6 -0
- package/src/agents/tools/connectAgent.ts +353 -0
- package/src/agents/tools/index.ts +6 -0
- package/src/agents/tools/list.ts +48 -0
- package/src/agents/tools/observe.ts +245 -0
- package/src/agents/tools/replyToParent.ts +122 -0
- package/src/agents/tools/runGenerate.ts +209 -0
- package/src/agents/tools/sendAgentMessage.ts +138 -0
- package/src/agents/tools/startAgentTask.ts +92 -5
- package/src/agents/tools/waitForAgentCompleted.ts +269 -0
- package/src/ai.ts +1 -0
- package/src/chat/modules/AgentModule.ts +89 -27
- package/src/chat/modules/SessionsModule.ts +6 -10
- package/src/clients/contextLimits.ts +26 -0
- package/src/clients/pricing/anthropic.ts +12 -0
- package/src/commands/agent.ts +22 -0
- package/src/commands/agents.ts +83 -0
- package/src/commands/misc.ts +6 -0
- package/src/commands/modules.ts +159 -7
- package/src/commands/services.ts +14 -0
- package/src/generate.ts +25 -3
- package/src/services/AgentService.ts +21 -3
- package/src/services/AgentSyncFs.ts +56 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/EventService.ts +4 -1
- package/src/services/KnowhowClient.ts +7 -0
- package/src/services/LazyToolsService.ts +7 -3
- package/src/services/MinimalToolsService.ts +15 -7
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +35 -6
- package/src/services/Tools.ts +43 -3
- package/src/services/modules/index.ts +127 -46
- package/src/types.ts +5 -0
- package/tests/agents/interrupt.test.ts +11 -18
- package/tests/agents/observe-base.test.ts +183 -0
- package/tests/agents/tools/connectAgent.test.ts +138 -0
- package/tests/agents/tools/observe.test.ts +179 -0
- package/tests/agents/tools/replyToParent.test.ts +125 -0
- package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
- package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
- package/tests/chat/detach-attach-rendering.test.ts +315 -0
- package/tests/clients/contextLimits.test.ts +23 -1
- package/tests/clients/pricing.test.ts +20 -1
- package/tests/services/Tools.test.ts +172 -9
- package/tests/unit/modules/moduleLoading.test.ts +69 -0
- package/ts_build/package.json +5 -2
- package/ts_build/src/agents/base/base.d.ts +24 -1
- package/ts_build/src/agents/base/base.js +111 -16
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/patcher/patcher.js +1 -1
- package/ts_build/src/agents/patcher/patcher.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +2 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
- package/ts_build/src/agents/tools/connectAgent.js +284 -0
- package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
- package/ts_build/src/agents/tools/index.d.ts +6 -0
- package/ts_build/src/agents/tools/index.js +6 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +40 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/observe.d.ts +20 -0
- package/ts_build/src/agents/tools/observe.js +144 -0
- package/ts_build/src/agents/tools/observe.js.map +1 -0
- package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
- package/ts_build/src/agents/tools/replyToParent.js +118 -0
- package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
- package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
- package/ts_build/src/agents/tools/runGenerate.js +138 -0
- package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
- package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
- package/ts_build/src/agents/tools/startAgentTask.js +53 -6
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
- package/ts_build/src/ai.d.ts +1 -0
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
- package/ts_build/src/chat/modules/AgentModule.js +46 -21
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -6
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +21 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
- package/ts_build/src/clients/pricing/anthropic.js +9 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/commands/agent.js +11 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.js +61 -0
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +2 -0
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/modules.js +86 -7
- package/ts_build/src/commands/modules.js.map +1 -1
- package/ts_build/src/commands/services.js +10 -0
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/generate.d.ts +2 -0
- package/ts_build/src/generate.js +11 -3
- package/ts_build/src/generate.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +4 -1
- package/ts_build/src/services/AgentService.js +9 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +35 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
- package/ts_build/src/services/EventService.d.ts +4 -1
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +3 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -2
- package/ts_build/src/services/LazyToolsService.js +2 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
- package/ts_build/src/services/MinimalToolsService.js +6 -6
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +2 -1
- package/ts_build/src/services/SyncerService.js +8 -2
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +6 -1
- package/ts_build/src/services/Tools.js +9 -3
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +1 -1
- package/ts_build/src/services/modules/index.js +67 -27
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
- package/ts_build/tests/agents/interrupt.test.js +3 -13
- package/ts_build/tests/agents/interrupt.test.js.map +1 -1
- package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
- package/ts_build/tests/agents/observe-base.test.js +149 -0
- package/ts_build/tests/agents/observe-base.test.js.map +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/observe.test.js +148 -0
- package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +11 -1
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
- package/ts_build/tests/clients/pricing.test.js +10 -1
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/services/Tools.test.js +126 -8
- package/ts_build/tests/services/Tools.test.js.map +1 -1
- package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
- 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(
|
|
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(
|
|
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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/services/Tools.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
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
|
-
|
|
86
|
+
candidates.push(path.resolve(process.cwd(), modulePath));
|
|
59
87
|
} else {
|
|
60
|
-
// For npm package names,
|
|
61
|
-
//
|
|
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
|
-
|
|
102
|
+
const resolved = require.resolve(modulePath);
|
|
103
|
+
if (!candidates.includes(resolved)) candidates.push(resolved);
|
|
64
104
|
} catch {
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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}": ${
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
8
|
-
*
|
|
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
|
|
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(); //
|
|
158
|
+
h.interrupt(); // nothing to interrupt -> no-op
|
|
165
159
|
|
|
166
|
-
//
|
|
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
|
-
|
|
170
|
-
|
|
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 () => {
|