@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.
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/hash.ts +3 -2
  25. package/src/commands/misc.ts +6 -0
  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/hash.js +1 -1
  104. package/ts_build/src/commands/hash.js.map +1 -1
  105. package/ts_build/src/commands/misc.js +2 -0
  106. package/ts_build/src/commands/misc.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
@@ -0,0 +1,353 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { Tool } from "../../clients/types";
4
+ import { ToolCallContext } from "../../services/Tools";
5
+
6
+ /** Resolve the agents dir lazily so tests (and cwd changes) are respected. */
7
+ function agentsDir(): string {
8
+ return path.join(process.cwd(), ".knowhow", "processes", "agents");
9
+ }
10
+
11
+ /**
12
+ * Active relays, keyed by connection id, so stopConnections can tear them down.
13
+ * A relay watches a speaker task's thread and appends new assistant messages to
14
+ * a listener task's input.txt.
15
+ */
16
+ interface ActiveRelay {
17
+ id: string;
18
+ listener: string;
19
+ speaker: string;
20
+ timer: NodeJS.Timeout;
21
+ expiry?: NodeJS.Timeout;
22
+ seenAssistant: number;
23
+ updates: number;
24
+ }
25
+
26
+ const relays = new Map<string, ActiveRelay>();
27
+ let relaySeq = 0;
28
+
29
+ function readMeta(taskId: string): any | null {
30
+ try {
31
+ const metaPath = path.join(agentsDir(), taskId, "metadata.json");
32
+ if (!fs.existsSync(metaPath)) return null;
33
+ return JSON.parse(fs.readFileSync(metaPath, "utf8"));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** Flatten a metadata.threads structure into a single message array. */
40
+ function flattenThreads(threads: any): any[] {
41
+ if (!threads) return [];
42
+ if (Array.isArray(threads)) {
43
+ if (threads.length && Array.isArray(threads[0])) return threads.flat();
44
+ return threads;
45
+ }
46
+ return [];
47
+ }
48
+
49
+ /** Extract the text content of an assistant message. */
50
+ function assistantText(msg: any): string | undefined {
51
+ if (!msg || msg.role !== "assistant") return undefined;
52
+ if (typeof msg.content === "string" && msg.content.trim()) {
53
+ return msg.content.trim();
54
+ }
55
+ if (Array.isArray(msg.content)) {
56
+ const text = msg.content
57
+ .filter((c: any) => c?.type === "text")
58
+ .map((c: any) => c.text)
59
+ .join("\n")
60
+ .trim();
61
+ if (text) return text;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ function appendToInput(inputPath: string, line: string): void {
67
+ const existing = fs.existsSync(inputPath)
68
+ ? fs.readFileSync(inputPath, "utf8")
69
+ : "";
70
+ const next =
71
+ existing && !existing.endsWith("\n")
72
+ ? `${existing}\n${line}\n`
73
+ : `${existing}${line}\n`;
74
+ fs.writeFileSync(inputPath, next, "utf8");
75
+ }
76
+
77
+ const TERMINAL_STATUSES = new Set([
78
+ "completed",
79
+ "failed",
80
+ "killed",
81
+ "error",
82
+ "done",
83
+ ]);
84
+
85
+ function isTerminal(meta: any | null): boolean {
86
+ if (!meta) return false;
87
+ if (meta.status && TERMINAL_STATUSES.has(meta.status)) return true;
88
+ if (meta.inProgress === false) return true;
89
+ return false;
90
+ }
91
+
92
+ export interface AgentConnection {
93
+ /** taskId of the agent that RECEIVES the speaker's messages. */
94
+ listener: string;
95
+ /** taskId of the agent whose output is relayed to the listener. */
96
+ speaker: string;
97
+ }
98
+
99
+ export interface ConnectAgentParams {
100
+ /**
101
+ * The connections to wire up. Each connection relays the speaker's new
102
+ * assistant messages into the listener's input queue.
103
+ *
104
+ * Topologies are expressed as arrays of connections, e.g.:
105
+ * bidirectional(a,b): [{listener:a,speaker:b},{listener:b,speaker:a}]
106
+ * pipeline(a,b,c): [{listener:b,speaker:a},{listener:c,speaker:b}]
107
+ * star(hub,[x,y]): [{listener:hub,speaker:x},{listener:hub,speaker:y},...]
108
+ * mesh([a,b,c]): every ordered pair.
109
+ */
110
+ connections: AgentConnection[];
111
+ /** Poll interval in ms for reading speaker threads. Default 3000. */
112
+ intervalMs?: number;
113
+ /** Max relayed messages per connection before auto-stopping. Default 50. */
114
+ maxUpdates?: number;
115
+ /** Max total duration in ms before auto-stopping each relay. Default 30 min. */
116
+ maxDurationMs?: number;
117
+ _ctx?: ToolCallContext;
118
+ }
119
+
120
+ /**
121
+ * Wire agents together so they can communicate while working. Each connection
122
+ * relays a speaker agent's new assistant messages into a listener agent's input
123
+ * queue (via sendAgentMessage-style input.txt appends). Because it accepts an
124
+ * ARRAY of connections, any topology — bidirectional, pipeline, ring, star,
125
+ * mesh — is expressible in a single call.
126
+ *
127
+ * Relays run in the background on the orchestrating process, dedupe by thread
128
+ * position, and auto-expire after maxUpdates / maxDurationMs so they can't leak.
129
+ * Call stopConnections to tear them down early.
130
+ */
131
+ export async function connectAgent(
132
+ params: ConnectAgentParams
133
+ ): Promise<string> {
134
+ const {
135
+ connections,
136
+ intervalMs = 3000,
137
+ maxUpdates = 50,
138
+ maxDurationMs = 30 * 60 * 1000,
139
+ } = params;
140
+
141
+ if (!Array.isArray(connections) || connections.length === 0) {
142
+ return "connectAgent: connections must be a non-empty array of { listener, speaker }.";
143
+ }
144
+
145
+ const interval = Math.max(1000, intervalMs);
146
+ const started: string[] = [];
147
+ const errors: string[] = [];
148
+
149
+ for (const conn of connections) {
150
+ const { listener, speaker } = conn || ({} as AgentConnection);
151
+ if (!listener || !speaker) {
152
+ errors.push(`skipped connection missing listener/speaker: ${JSON.stringify(conn)}`);
153
+ continue;
154
+ }
155
+ if (listener === speaker) {
156
+ errors.push(`skipped self-connection for ${listener}`);
157
+ continue;
158
+ }
159
+
160
+ const speakerDir = path.join(agentsDir(), speaker);
161
+ const listenerDir = path.join(agentsDir(), listener);
162
+ if (!fs.existsSync(speakerDir)) {
163
+ errors.push(`speaker task dir not found: ${speaker}`);
164
+ continue;
165
+ }
166
+ if (!fs.existsSync(listenerDir)) {
167
+ errors.push(`listener task dir not found: ${listener}`);
168
+ continue;
169
+ }
170
+
171
+ relaySeq += 1;
172
+ const id = `conn_${speaker.slice(0, 8)}_to_${listener.slice(0, 8)}_${relaySeq}`;
173
+ const listenerInput = path.join(listenerDir, "input.txt");
174
+
175
+ // Seed the seen-count to the CURRENT number of assistant messages so we
176
+ // only relay NEW output produced after the connection is established.
177
+ const seedMeta = readMeta(speaker);
178
+ const seedAssistants = flattenThreads(seedMeta?.threads).filter(
179
+ (m) => m?.role === "assistant"
180
+ ).length;
181
+
182
+ const relay: ActiveRelay = {
183
+ id,
184
+ listener,
185
+ speaker,
186
+ timer: undefined as any,
187
+ seenAssistant: seedAssistants,
188
+ updates: 0,
189
+ };
190
+
191
+ const tick = () => {
192
+ try {
193
+ const meta = readMeta(speaker);
194
+ const assistants = flattenThreads(meta?.threads).filter(
195
+ (m) => m?.role === "assistant"
196
+ );
197
+ // Relay any assistant messages beyond what we've seen.
198
+ for (let i = relay.seenAssistant; i < assistants.length; i++) {
199
+ const text = assistantText(assistants[i]);
200
+ if (text) {
201
+ appendToInput(
202
+ listenerInput,
203
+ `[from:${speaker}] ${text}`
204
+ );
205
+ relay.updates += 1;
206
+ }
207
+ }
208
+ relay.seenAssistant = assistants.length;
209
+
210
+ if (relay.updates >= maxUpdates || isTerminal(meta)) {
211
+ stopRelay(id);
212
+ }
213
+ } catch {
214
+ // ignore transient read/write errors; try again next tick
215
+ }
216
+ };
217
+
218
+ relay.timer = setInterval(tick, interval);
219
+ if (typeof (relay.timer as any).unref === "function") {
220
+ (relay.timer as any).unref();
221
+ }
222
+ if (maxDurationMs) {
223
+ relay.expiry = setTimeout(() => stopRelay(id), maxDurationMs);
224
+ if (typeof (relay.expiry as any).unref === "function") {
225
+ (relay.expiry as any).unref();
226
+ }
227
+ }
228
+
229
+ relays.set(id, relay);
230
+ started.push(id);
231
+ }
232
+
233
+ const parts = [
234
+ `Wired ${started.length} connection(s): ${started.join(", ") || "(none)"}.`,
235
+ ];
236
+ if (errors.length) {
237
+ parts.push(`Issues: ${errors.join("; ")}.`);
238
+ }
239
+ parts.push(
240
+ `Relays poll every ${interval}ms, up to ${maxUpdates} messages each. Call stopConnections to tear them down.`
241
+ );
242
+ return parts.join(" ");
243
+ }
244
+
245
+ function stopRelay(id: string): void {
246
+ const relay = relays.get(id);
247
+ if (!relay) return;
248
+ if (relay.timer) clearInterval(relay.timer);
249
+ if (relay.expiry) clearTimeout(relay.expiry);
250
+ relays.delete(id);
251
+ }
252
+
253
+ export interface StopConnectionsParams {
254
+ /** Connection id to stop. Omit to stop ALL active relays. */
255
+ id?: string;
256
+ _ctx?: ToolCallContext;
257
+ }
258
+
259
+ /** Tear down one relay (by id) or all of them. Companion to connectAgent. */
260
+ export async function stopConnections(
261
+ params: StopConnectionsParams
262
+ ): Promise<string> {
263
+ const { id } = params;
264
+ if (id) {
265
+ const existed = relays.has(id);
266
+ stopRelay(id);
267
+ return existed
268
+ ? `Stopped connection ${id}.`
269
+ : `No active connection with id ${id}.`;
270
+ }
271
+ const count = relays.size;
272
+ for (const rid of [...relays.keys()]) stopRelay(rid);
273
+ return `Stopped ${count} active connection(s).`;
274
+ }
275
+
276
+ export const connectAgentDefinition: Tool = {
277
+ type: "function",
278
+ function: {
279
+ name: "connectAgent",
280
+ description:
281
+ "Wire running agents together so they can communicate while working. Takes an ARRAY of " +
282
+ "{ listener, speaker } connections — each relays the speaker's new assistant messages into the " +
283
+ "listener's input queue. Because it accepts an array, ANY topology is one call: bidirectional " +
284
+ "([{listener:a,speaker:b},{listener:b,speaker:a}]), pipeline, ring, star (many speakers → one hub), " +
285
+ "or full mesh (every ordered pair). Relays run in the background, dedupe by thread position, and " +
286
+ "auto-expire (or stop when the speaker completes). Use stopConnections to tear them down early. " +
287
+ "Combine with startAgentTask (spawn) and waitForAgentCompleted (join).",
288
+ parameters: {
289
+ type: "object",
290
+ positional: false,
291
+ properties: {
292
+ connections: {
293
+ type: "array",
294
+ description:
295
+ "Array of connections to establish. Each is { listener, speaker } where speaker's output " +
296
+ "is relayed into listener's input queue.",
297
+ items: {
298
+ type: "object",
299
+ properties: {
300
+ listener: {
301
+ type: "string",
302
+ description: "taskId of the agent that receives messages.",
303
+ },
304
+ speaker: {
305
+ type: "string",
306
+ description:
307
+ "taskId of the agent whose output is relayed to the listener.",
308
+ },
309
+ },
310
+ },
311
+ },
312
+ intervalMs: {
313
+ type: "number",
314
+ description:
315
+ "Poll interval in ms for reading speaker threads. Default: 3000.",
316
+ },
317
+ maxUpdates: {
318
+ type: "number",
319
+ description:
320
+ "Max relayed messages per connection before auto-stopping. Default: 50.",
321
+ },
322
+ maxDurationMs: {
323
+ type: "number",
324
+ description:
325
+ "Max total duration in ms before auto-stopping each relay. Default: 1800000 (30 min).",
326
+ },
327
+ },
328
+ required: ["connections"],
329
+ },
330
+ },
331
+ };
332
+
333
+ export const stopConnectionsDefinition: Tool = {
334
+ type: "function",
335
+ function: {
336
+ name: "stopConnections",
337
+ description:
338
+ "Tear down agent connections established with connectAgent. Pass a connection id to stop a " +
339
+ "specific relay, or omit id to stop ALL active relays.",
340
+ parameters: {
341
+ type: "object",
342
+ positional: false,
343
+ properties: {
344
+ id: {
345
+ type: "string",
346
+ description:
347
+ "The connection id returned by connectAgent. Omit to stop ALL active connections.",
348
+ },
349
+ },
350
+ required: [],
351
+ },
352
+ },
353
+ };
@@ -21,6 +21,12 @@ export * from "./aiClient";
21
21
  export * from "./googleSearch";
22
22
  export * from "./stringReplace";
23
23
  export * from "./startAgentTask";
24
+ export * from "./replyToParent";
25
+ export * from "./runGenerate";
26
+ export * from "./sendAgentMessage";
27
+ export * from "./connectAgent";
28
+ export * from "./waitForAgentCompleted";
29
+ export * from "./observe";
24
30
  export * from "./ycmd";
25
31
  export * from "./mcp";
26
32
  export * from "./skills";
@@ -7,6 +7,18 @@ import * as language from "./language/definitions";
7
7
  import * as mcp from "./mcp/definitions";
8
8
  import { googleSearchDefinition } from "./googleSearch";
9
9
  import { startAgentTaskDefinition } from "./startAgentTask";
10
+ import { replyToParentDefinition } from "./replyToParent";
11
+ import { runGenerateDefinition } from "./runGenerate";
12
+ import { sendAgentMessageDefinition } from "./sendAgentMessage";
13
+ import {
14
+ connectAgentDefinition,
15
+ stopConnectionsDefinition,
16
+ } from "./connectAgent";
17
+ import { waitForAgentCompletedDefinition } from "./waitForAgentCompleted";
18
+ import {
19
+ observeDefinition,
20
+ stopObservingDefinition,
21
+ } from "./observe";
10
22
  import { skillsToolDefinitions } from "./skills";
11
23
 
12
24
  function getPluginNames(): string {
@@ -19,6 +31,34 @@ function getPluginNames(): string {
19
31
  }
20
32
 
21
33
  export const includedTools = [
34
+ {
35
+ type: "function",
36
+ function: {
37
+ name: "agentCall",
38
+ description:
39
+ "Run another registered agent in-process as a blocking call and get its final answer back as a string. Unlike a plain llm() call, the target agent can use tools, loop, and maintain its own context. Useful as a graph node that delegates to a specialized agent (e.g. Researcher, Patcher). For long-running / parallel / detached work prefer startAgentTask.",
40
+ parameters: {
41
+ type: "object",
42
+ positional: true,
43
+ properties: {
44
+ agentName: {
45
+ type: "string",
46
+ description:
47
+ "The name of the agent to call (e.g. Researcher, Developer, Patcher).",
48
+ },
49
+ query: {
50
+ type: "string",
51
+ description: "The query / task to send to the agent.",
52
+ },
53
+ },
54
+ required: ["agentName", "query"],
55
+ },
56
+ returns: {
57
+ type: "string",
58
+ description: "The agent's final answer text.",
59
+ },
60
+ },
61
+ },
22
62
  {
23
63
  type: "function",
24
64
  function: {
@@ -679,6 +719,14 @@ export const includedTools = [
679
719
  },
680
720
  googleSearchDefinition,
681
721
  startAgentTaskDefinition,
722
+ replyToParentDefinition,
723
+ runGenerateDefinition,
724
+ sendAgentMessageDefinition,
725
+ connectAgentDefinition,
726
+ stopConnectionsDefinition,
727
+ waitForAgentCompletedDefinition,
728
+ observeDefinition,
729
+ stopObservingDefinition,
682
730
  ...ycmd.definitions,
683
731
  ...language.definitions,
684
732
  ...mcp.definitions,
@@ -0,0 +1,245 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { ToolCallContext } from "../../services/Tools";
3
+ import { ObservationSource } from "../base/base";
4
+
5
+ /**
6
+ * Build an `ObservationSource` that repeatedly invokes another tool on an
7
+ * interval and emits its (stringified) result. This is the "tool poll" adapter
8
+ * — the original behavior of `observe`, now expressed as a source so it plugs
9
+ * into the unified `agent.observe()` subscription primitive on BaseAgent.
10
+ */
11
+ function toolSource(
12
+ toolName: string,
13
+ toolArgs: Record<string, any>,
14
+ intervalMs: number,
15
+ caller: any,
16
+ Tools: any,
17
+ label: string
18
+ ): ObservationSource {
19
+ return {
20
+ label,
21
+ start(emit) {
22
+ const enabledTools =
23
+ typeof caller.getEnabledToolNames === "function"
24
+ ? caller.getEnabledToolNames()
25
+ : undefined;
26
+ const interval = Math.max(500, intervalMs);
27
+ let seq = 0;
28
+
29
+ const runOnce = async () => {
30
+ let resultStr: string;
31
+ try {
32
+ const toolCall = {
33
+ id: `obs_${toolName}_${seq++}`,
34
+ type: "function",
35
+ function: {
36
+ name: toolName,
37
+ arguments: JSON.stringify(toolArgs),
38
+ },
39
+ } as any;
40
+ const raw = await Tools.callTool(toolCall, enabledTools, {
41
+ caller,
42
+ taskId: caller?.currentTaskId,
43
+ });
44
+ resultStr = typeof raw === "string" ? raw : JSON.stringify(raw);
45
+ } catch (e: any) {
46
+ resultStr = `[error] ${e?.message ?? String(e)}`;
47
+ }
48
+ emit(resultStr);
49
+ };
50
+
51
+ const timer = setInterval(() => {
52
+ runOnce().catch(() => {});
53
+ }, interval);
54
+ if (typeof (timer as any).unref === "function") (timer as any).unref();
55
+
56
+ // teardown
57
+ return () => clearInterval(timer);
58
+ },
59
+ };
60
+ }
61
+
62
+ export interface ObserveParams {
63
+ /** The name of a tool to invoke repeatedly. */
64
+ toolName: string;
65
+ /** Arguments to pass to that tool on each invocation. */
66
+ toolArgs?: Record<string, any>;
67
+ /** Interval between invocations in milliseconds. Default 5000ms. */
68
+ intervalMs?: number;
69
+ /** Maximum number of updates before auto-stopping. Default 20. */
70
+ maxUpdates?: number;
71
+ /** Maximum total duration in milliseconds before auto-stopping. Default 10 min. */
72
+ maxDurationMs?: number;
73
+ /** Optional human-friendly label describing what's being observed. */
74
+ label?: string;
75
+ /**
76
+ * When true, only deliver an update to the caller if the tool result changed
77
+ * since the last poll. Default true (reduces noise).
78
+ */
79
+ onlyOnChange?: boolean;
80
+ _ctx?: ToolCallContext;
81
+ }
82
+
83
+ /**
84
+ * Periodically invoke another tool and stream its results back to the calling
85
+ * agent as pending context messages. Returns an observation id that can be
86
+ * passed to `stopObserving` to cancel early.
87
+ *
88
+ * This is a self-referential tool: it reads the calling agent off `_ctx.caller`
89
+ * and the ToolsService off `_ctx.Tools`, captures both synchronously (so
90
+ * concurrent tool calls on a shared ToolsService can't clobber the reference),
91
+ * and delegates to the agent's own `observe()` method — so the observation is
92
+ * scoped to and owned by that agent instance (no cross-agent clobber, torn down
93
+ * on task boundaries).
94
+ */
95
+ export async function observe(params: ObserveParams): Promise<string> {
96
+ const {
97
+ toolName,
98
+ toolArgs = {},
99
+ intervalMs = 5000,
100
+ maxUpdates = 20,
101
+ maxDurationMs = 10 * 60 * 1000,
102
+ label,
103
+ onlyOnChange = true,
104
+ _ctx,
105
+ } = params;
106
+
107
+ if (!toolName) {
108
+ return "observe: toolName is required.";
109
+ }
110
+
111
+ // Capture the caller + Tools synchronously — do NOT read them lazily inside
112
+ // the interval, since the shared per-call context may change by then.
113
+ const caller = _ctx?.caller;
114
+ const Tools = _ctx?.Tools;
115
+
116
+ if (!Tools || typeof Tools.callTool !== "function") {
117
+ return "observe: no ToolsService available on _ctx to invoke the observed tool.";
118
+ }
119
+ if (!caller || typeof caller.observe !== "function") {
120
+ return "observe: no calling agent available on _ctx to receive updates.";
121
+ }
122
+
123
+ const displayLabel = label || `${toolName}(${JSON.stringify(toolArgs)})`;
124
+ const source = toolSource(
125
+ toolName,
126
+ toolArgs,
127
+ intervalMs,
128
+ caller,
129
+ Tools,
130
+ displayLabel
131
+ );
132
+
133
+ const id = caller.observe(source, {
134
+ onlyOnChange,
135
+ maxUpdates,
136
+ maxDurationMs,
137
+ });
138
+
139
+ const interval = Math.max(500, intervalMs);
140
+ return (
141
+ `Started observing "${displayLabel}" (id: ${id}). ` +
142
+ `Polling every ${interval}ms, up to ${maxUpdates} updates or ${Math.round(
143
+ maxDurationMs / 1000
144
+ )}s. ` +
145
+ `Updates will be delivered to you as they arrive. Call stopObserving with this id to cancel early.`
146
+ );
147
+ }
148
+
149
+ export interface StopObservingParams {
150
+ /** The observation id returned by observe. Omit to stop ALL observers. */
151
+ id?: string;
152
+ _ctx?: ToolCallContext;
153
+ }
154
+
155
+ /**
156
+ * Cancel an active observer (or all of them). Companion to `observe`.
157
+ * Delegates to the calling agent's own `stopObserving()`.
158
+ */
159
+ export async function stopObserving(
160
+ params: StopObservingParams
161
+ ): Promise<string> {
162
+ const { id, _ctx } = params;
163
+ const caller = _ctx?.caller;
164
+
165
+ if (!caller || typeof caller.stopObserving !== "function") {
166
+ return "stopObserving: no calling agent available on _ctx.";
167
+ }
168
+
169
+ return caller.stopObserving(id, "stopped by stopObserving");
170
+ }
171
+
172
+ export const observeDefinition: Tool = {
173
+ type: "function",
174
+ function: {
175
+ name: "observe",
176
+ description:
177
+ "Periodically invoke another tool in the background and stream its results back to you as " +
178
+ "context updates, without blocking. Useful for watching a long-running process — e.g. observe " +
179
+ "waitForAgentCompleted, agents status, or execCommand tailing a log — and reacting when things " +
180
+ "change (by default only changed results are delivered). Returns an observation id; auto-expires " +
181
+ "after maxUpdates or maxDurationMs. Call stopObserving with the id to cancel early.",
182
+ parameters: {
183
+ type: "object",
184
+ positional: false,
185
+ properties: {
186
+ toolName: {
187
+ type: "string",
188
+ description: "The name of the tool to invoke repeatedly.",
189
+ },
190
+ toolArgs: {
191
+ type: "object",
192
+ description:
193
+ "The arguments object to pass to the observed tool on each poll.",
194
+ },
195
+ intervalMs: {
196
+ type: "number",
197
+ description: "Interval between polls in milliseconds. Default: 5000.",
198
+ },
199
+ maxUpdates: {
200
+ type: "number",
201
+ description:
202
+ "Maximum number of updates before auto-stopping. Default: 20.",
203
+ },
204
+ maxDurationMs: {
205
+ type: "number",
206
+ description:
207
+ "Maximum total observation duration in milliseconds before auto-stopping. Default: 600000 (10 min).",
208
+ },
209
+ label: {
210
+ type: "string",
211
+ description:
212
+ "Optional human-friendly label describing what's being observed.",
213
+ },
214
+ onlyOnChange: {
215
+ type: "boolean",
216
+ description:
217
+ "When true (default), only deliver an update when the tool result changed since the last poll.",
218
+ },
219
+ },
220
+ required: ["toolName"],
221
+ },
222
+ },
223
+ };
224
+
225
+ export const stopObservingDefinition: Tool = {
226
+ type: "function",
227
+ function: {
228
+ name: "stopObserving",
229
+ description:
230
+ "Cancel an active observer started with the observe tool. Pass the observation id to stop a " +
231
+ "specific one, or omit id to stop all active observers.",
232
+ parameters: {
233
+ type: "object",
234
+ positional: false,
235
+ properties: {
236
+ id: {
237
+ type: "string",
238
+ description:
239
+ "The observation id returned by observe. Omit to stop ALL active observers.",
240
+ },
241
+ },
242
+ required: [],
243
+ },
244
+ },
245
+ };