@tyvm/knowhow 0.0.137 → 0.0.140
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/hash.ts +3 -2
- package/src/commands/misc.ts +6 -0
- 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/hash.js +1 -1
- package/ts_build/src/commands/hash.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/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
|
@@ -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 () => {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for BaseAgent.observe() / stopObserving() — the per-agent observation
|
|
3
|
+
* registry that the `observe` tool delegates to.
|
|
4
|
+
*
|
|
5
|
+
* Verifies:
|
|
6
|
+
* 1. A source's emit() is delivered to the agent as a pending message.
|
|
7
|
+
* 2. onlyOnChange dedupes repeated data.
|
|
8
|
+
* 3. maxUpdates auto-stops and runs the source teardown.
|
|
9
|
+
* 4. stopObserving(id) cancels one; stopObserving() cancels all.
|
|
10
|
+
* 5. Observations are scoped to the instance — one agent's stopObserving()
|
|
11
|
+
* with no id does NOT affect another agent's observers.
|
|
12
|
+
* 6. Data emitted after the agent is "done" is not injected.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { BaseAgent, ObservationSource } from "../../src/agents/base/base";
|
|
16
|
+
|
|
17
|
+
// Teardown runs on the microtask queue (Promise.resolve(teardownPromise).then),
|
|
18
|
+
// so flush a couple of ticks before asserting torndown state.
|
|
19
|
+
const flush = () => new Promise((r) => setTimeout(r, 0));
|
|
20
|
+
|
|
21
|
+
// Minimal stubs so BaseAgent can be constructed.
|
|
22
|
+
function makeStubs() {
|
|
23
|
+
const events: any = {
|
|
24
|
+
setListener: () => {},
|
|
25
|
+
emit: () => {},
|
|
26
|
+
on: () => {},
|
|
27
|
+
};
|
|
28
|
+
const tools: any = {
|
|
29
|
+
getContext: () => ({}),
|
|
30
|
+
callTool: async () => "",
|
|
31
|
+
};
|
|
32
|
+
return { events, tools };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class TestAgent extends BaseAgent {
|
|
36
|
+
name = "TestAgent";
|
|
37
|
+
description = "test";
|
|
38
|
+
constructor() {
|
|
39
|
+
const { events, tools } = makeStubs();
|
|
40
|
+
super({ Tools: tools, Events: events });
|
|
41
|
+
// Agent must not be "done" for observations to deliver.
|
|
42
|
+
(this as any).status = this.eventTypes.inProgress;
|
|
43
|
+
}
|
|
44
|
+
async getInitialMessages(): Promise<any[]> {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
get delivered(): any[] {
|
|
48
|
+
return (this as any).pendingMessages;
|
|
49
|
+
}
|
|
50
|
+
setStatus(s: string) {
|
|
51
|
+
(this as any).status = s;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A source we can drive manually and that records teardown. */
|
|
56
|
+
function manualSource(label: string) {
|
|
57
|
+
let emitFn: ((d: unknown) => void) | undefined;
|
|
58
|
+
const state = { torndown: false };
|
|
59
|
+
const source: ObservationSource = {
|
|
60
|
+
label,
|
|
61
|
+
start(emit) {
|
|
62
|
+
emitFn = emit;
|
|
63
|
+
return () => {
|
|
64
|
+
state.torndown = true;
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
source,
|
|
70
|
+
push: (d: unknown) => emitFn && emitFn(d),
|
|
71
|
+
state,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("BaseAgent.observe / stopObserving", () => {
|
|
76
|
+
it("delivers emitted data as pending messages", () => {
|
|
77
|
+
const agent = new TestAgent();
|
|
78
|
+
const { source, push } = manualSource("thing");
|
|
79
|
+
const id = agent.observe(source);
|
|
80
|
+
expect(id).toMatch(/^obs_thing_/);
|
|
81
|
+
|
|
82
|
+
push("hello");
|
|
83
|
+
expect(agent.delivered.length).toBe(1);
|
|
84
|
+
expect(String(agent.delivered[0].content)).toContain("hello");
|
|
85
|
+
expect(String(agent.delivered[0].content)).toContain(`[observe:${id}]`);
|
|
86
|
+
agent.stopObserving();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("dedupes identical consecutive data with onlyOnChange (default)", () => {
|
|
90
|
+
const agent = new TestAgent();
|
|
91
|
+
const { source, push } = manualSource("dup");
|
|
92
|
+
agent.observe(source);
|
|
93
|
+
push("a");
|
|
94
|
+
push("a");
|
|
95
|
+
push("b");
|
|
96
|
+
expect(agent.delivered.length).toBe(2);
|
|
97
|
+
agent.stopObserving();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("delivers every emit when onlyOnChange is false", () => {
|
|
101
|
+
const agent = new TestAgent();
|
|
102
|
+
const { source, push } = manualSource("nochange");
|
|
103
|
+
// dedupe of pendingMessages by content still applies in addPendingMessage,
|
|
104
|
+
// so use distinct data to observe count.
|
|
105
|
+
agent.observe(source, { onlyOnChange: false });
|
|
106
|
+
push("x1");
|
|
107
|
+
push("x2");
|
|
108
|
+
push("x3");
|
|
109
|
+
expect(agent.delivered.length).toBe(3);
|
|
110
|
+
agent.stopObserving();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("auto-stops after maxUpdates and runs teardown", async () => {
|
|
114
|
+
const agent = new TestAgent();
|
|
115
|
+
const { source, push, state } = manualSource("cap");
|
|
116
|
+
agent.observe(source, { maxUpdates: 2 });
|
|
117
|
+
push("1");
|
|
118
|
+
push("2"); // hits cap -> stop
|
|
119
|
+
push("3"); // ignored, observer gone
|
|
120
|
+
expect(agent.delivered.length).toBe(2);
|
|
121
|
+
await flush();
|
|
122
|
+
expect(state.torndown).toBe(true);
|
|
123
|
+
agent.stopObserving();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("stopObserving(id) cancels one observer and tears it down", async () => {
|
|
127
|
+
const agent = new TestAgent();
|
|
128
|
+
const a = manualSource("a");
|
|
129
|
+
const b = manualSource("b");
|
|
130
|
+
const idA = agent.observe(a.source);
|
|
131
|
+
agent.observe(b.source);
|
|
132
|
+
const res = agent.stopObserving(idA);
|
|
133
|
+
expect(res).toContain(`Stopped observer ${idA}`);
|
|
134
|
+
await flush();
|
|
135
|
+
expect(a.state.torndown).toBe(true);
|
|
136
|
+
expect(b.state.torndown).toBe(false);
|
|
137
|
+
// b still delivers
|
|
138
|
+
b.push("still here");
|
|
139
|
+
expect(agent.delivered.some((m) => String(m.content).includes("still here"))).toBe(true);
|
|
140
|
+
agent.stopObserving();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("stopObserving() with no id cancels all", async () => {
|
|
144
|
+
const agent = new TestAgent();
|
|
145
|
+
const a = manualSource("a");
|
|
146
|
+
const b = manualSource("b");
|
|
147
|
+
agent.observe(a.source);
|
|
148
|
+
agent.observe(b.source);
|
|
149
|
+
const res = agent.stopObserving();
|
|
150
|
+
expect(res).toMatch(/Stopped 2 active observer/);
|
|
151
|
+
await flush();
|
|
152
|
+
expect(a.state.torndown).toBe(true);
|
|
153
|
+
expect(b.state.torndown).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("is scoped per-agent — one agent's stopObserving does not affect another", async () => {
|
|
157
|
+
const agentA = new TestAgent();
|
|
158
|
+
const agentB = new TestAgent();
|
|
159
|
+
const sa = manualSource("sa");
|
|
160
|
+
const sb = manualSource("sb");
|
|
161
|
+
agentA.observe(sa.source);
|
|
162
|
+
agentB.observe(sb.source);
|
|
163
|
+
|
|
164
|
+
agentA.stopObserving(); // should only affect A
|
|
165
|
+
await flush();
|
|
166
|
+
expect(sa.state.torndown).toBe(true);
|
|
167
|
+
expect(sb.state.torndown).toBe(false);
|
|
168
|
+
|
|
169
|
+
sb.push("b-data");
|
|
170
|
+
expect(agentB.delivered.some((m) => String(m.content).includes("b-data"))).toBe(true);
|
|
171
|
+
agentB.stopObserving();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not inject data once the agent is done", () => {
|
|
175
|
+
const agent = new TestAgent();
|
|
176
|
+
const { source, push } = manualSource("late");
|
|
177
|
+
agent.observe(source);
|
|
178
|
+
agent.setStatus(agent.eventTypes.done);
|
|
179
|
+
push("too late");
|
|
180
|
+
expect(agent.delivered.length).toBe(0);
|
|
181
|
+
agent.stopObserving();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import {
|
|
5
|
+
connectAgent,
|
|
6
|
+
stopConnections,
|
|
7
|
+
} from "../../../src/agents/tools/connectAgent";
|
|
8
|
+
|
|
9
|
+
describe("connectAgent / stopConnections", () => {
|
|
10
|
+
let tmpCwd: string;
|
|
11
|
+
let originalCwd: string;
|
|
12
|
+
let agentsDir: string;
|
|
13
|
+
|
|
14
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalCwd = process.cwd();
|
|
18
|
+
tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), "connect-agent-"));
|
|
19
|
+
process.chdir(tmpCwd);
|
|
20
|
+
agentsDir = path.join(tmpCwd, ".knowhow", "processes", "agents");
|
|
21
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await stopConnections({});
|
|
26
|
+
process.chdir(originalCwd);
|
|
27
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function makeTask(taskId: string, threads: any[] = []): string {
|
|
31
|
+
const dir = path.join(agentsDir, taskId);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
writeThreads(taskId, threads);
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeThreads(taskId: string, threads: any[], extra: any = {}) {
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(agentsDir, taskId, "metadata.json"),
|
|
40
|
+
JSON.stringify({ taskId, threads, ...extra }, null, 2),
|
|
41
|
+
"utf8"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readInput(taskId: string): string {
|
|
46
|
+
const p = path.join(agentsDir, taskId, "input.txt");
|
|
47
|
+
return fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it("rejects empty connections array", async () => {
|
|
51
|
+
const res = await connectAgent({ connections: [] });
|
|
52
|
+
expect(res).toMatch(/non-empty array/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("reports missing task dirs but still wires valid ones", async () => {
|
|
56
|
+
makeTask("a");
|
|
57
|
+
const res = await connectAgent({
|
|
58
|
+
connections: [
|
|
59
|
+
{ listener: "a", speaker: "missing" },
|
|
60
|
+
{ listener: "a", speaker: "a" }, // self connection skipped
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
expect(res).toMatch(/speaker task dir not found: missing/);
|
|
64
|
+
expect(res).toMatch(/self-connection for a/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("relays only NEW assistant messages from speaker to listener", async () => {
|
|
68
|
+
// Speaker starts with one existing assistant message (should NOT be relayed)
|
|
69
|
+
makeTask("listener", []);
|
|
70
|
+
makeTask("speaker", [
|
|
71
|
+
{ role: "assistant", content: "old message" },
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
await connectAgent({
|
|
75
|
+
connections: [{ listener: "listener", speaker: "speaker" }],
|
|
76
|
+
intervalMs: 1000,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Now the speaker produces a new assistant message
|
|
80
|
+
writeThreads("speaker", [
|
|
81
|
+
{ role: "assistant", content: "old message" },
|
|
82
|
+
{ role: "assistant", content: "new insight" },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// Wait for a couple of poll cycles
|
|
86
|
+
await delay(1300);
|
|
87
|
+
|
|
88
|
+
const input = readInput("listener");
|
|
89
|
+
expect(input).toContain("[from:speaker] new insight");
|
|
90
|
+
expect(input).not.toContain("old message");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("supports bidirectional in a single call (array of connections)", async () => {
|
|
94
|
+
makeTask("a", []);
|
|
95
|
+
makeTask("b", []);
|
|
96
|
+
|
|
97
|
+
await connectAgent({
|
|
98
|
+
connections: [
|
|
99
|
+
{ listener: "a", speaker: "b" },
|
|
100
|
+
{ listener: "b", speaker: "a" },
|
|
101
|
+
],
|
|
102
|
+
intervalMs: 1000,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
writeThreads("a", [{ role: "assistant", content: "from A" }]);
|
|
106
|
+
writeThreads("b", [{ role: "assistant", content: "from B" }]);
|
|
107
|
+
|
|
108
|
+
await delay(1300);
|
|
109
|
+
|
|
110
|
+
// A hears B, B hears A
|
|
111
|
+
expect(readInput("a")).toContain("[from:b] from B");
|
|
112
|
+
expect(readInput("b")).toContain("[from:a] from A");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("stops a specific connection by id", async () => {
|
|
116
|
+
makeTask("l", []);
|
|
117
|
+
makeTask("s", []);
|
|
118
|
+
const res = await connectAgent({
|
|
119
|
+
connections: [{ listener: "l", speaker: "s" }],
|
|
120
|
+
intervalMs: 1000,
|
|
121
|
+
});
|
|
122
|
+
const idMatch = res.match(/conn_[A-Za-z0-9_]+/);
|
|
123
|
+
expect(idMatch).toBeTruthy();
|
|
124
|
+
const stopRes = await stopConnections({ id: idMatch![0] });
|
|
125
|
+
expect(stopRes).toMatch(/Stopped connection/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("stopConnections() with no id stops all", async () => {
|
|
129
|
+
makeTask("l2", []);
|
|
130
|
+
makeTask("s2", []);
|
|
131
|
+
await connectAgent({
|
|
132
|
+
connections: [{ listener: "l2", speaker: "s2" }],
|
|
133
|
+
intervalMs: 1000,
|
|
134
|
+
});
|
|
135
|
+
const res = await stopConnections({});
|
|
136
|
+
expect(res).toMatch(/Stopped 1 active connection/);
|
|
137
|
+
});
|
|
138
|
+
});
|