@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,122 @@
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
+ * Resolve the parent taskId for the calling agent.
13
+ *
14
+ * Order of resolution:
15
+ * 1. `_ctx.parentTaskId` if the caller carries it directly
16
+ * 2. `_ctx.caller.parentTaskId` if the agent instance exposes it
17
+ * 3. The `parentTaskId` field of the caller's own metadata.json (looked up by
18
+ * `_ctx.taskId`)
19
+ */
20
+ function resolveParentTaskId(ctx?: ToolCallContext): string | undefined {
21
+ if (!ctx) return undefined;
22
+
23
+ if (ctx.parentTaskId) return ctx.parentTaskId as string;
24
+ const callerParent = (ctx.caller as any)?.parentTaskId;
25
+ if (callerParent) return callerParent as string;
26
+
27
+ // Fall back to reading our own metadata.json for the parentTaskId field.
28
+ const ownTaskId = ctx.taskId ?? (ctx.caller as any)?.currentTaskId;
29
+ if (!ownTaskId) return undefined;
30
+
31
+ try {
32
+ const metaPath = path.join(agentsDir(), ownTaskId, "metadata.json");
33
+ if (fs.existsSync(metaPath)) {
34
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
35
+ return meta.parentTaskId;
36
+ }
37
+ } catch {
38
+ // ignore — no parent metadata available
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ /**
44
+ * Send a message back to the parent agent that spawned this task.
45
+ *
46
+ * This is a self-referential tool: it uses the per-call context (`_ctx`) to
47
+ * discover which agent is calling it and which parent task spawned it, then
48
+ * appends the message to the parent's `input.txt` so the parent picks it up on
49
+ * its next sync cycle.
50
+ *
51
+ * `_ctx` is threaded in by ToolsService.callTool as the LAST positional
52
+ * argument. The agent effectively passes *itself* in via `_ctx.caller`.
53
+ */
54
+ export async function replyToParent(
55
+ message: string,
56
+ _ctx?: ToolCallContext
57
+ ): Promise<string> {
58
+ if (!message || !message.trim()) {
59
+ return "No message provided to replyToParent.";
60
+ }
61
+
62
+ const parentTaskId = resolveParentTaskId(_ctx);
63
+ if (!parentTaskId) {
64
+ return (
65
+ "No parent task found — this agent was not spawned with a parent taskId, " +
66
+ "so there is no one to reply to."
67
+ );
68
+ }
69
+
70
+ const parentDir = path.join(agentsDir(), parentTaskId);
71
+ const parentInputPath = path.join(parentDir, "input.txt");
72
+
73
+ if (!fs.existsSync(parentDir)) {
74
+ return `Parent task directory not found: ${parentDir}`;
75
+ }
76
+
77
+ const fromTaskId =
78
+ _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId ?? "unknown";
79
+ const fromAgent = (_ctx?.caller as any)?.name ?? "child";
80
+
81
+ // Envelope with sender identity so the parent knows who is replying.
82
+ const envelope = `[from:${fromTaskId}] [agent:${fromAgent}] ${message}`;
83
+
84
+ try {
85
+ // Append rather than overwrite so we don't clobber other pending messages.
86
+ const existing = fs.existsSync(parentInputPath)
87
+ ? fs.readFileSync(parentInputPath, "utf8")
88
+ : "";
89
+ const next = existing && !existing.endsWith("\n")
90
+ ? `${existing}\n${envelope}\n`
91
+ : `${existing}${envelope}\n`;
92
+ fs.writeFileSync(parentInputPath, next, "utf8");
93
+ } catch (e: any) {
94
+ return `Failed to write to parent input.txt: ${e?.message ?? String(e)}`;
95
+ }
96
+
97
+ return `Sent message to parent task ${parentTaskId}.`;
98
+ }
99
+
100
+ export const replyToParentDefinition: Tool = {
101
+ type: "function",
102
+ function: {
103
+ name: "replyToParent",
104
+ description:
105
+ "Send a message back to the parent agent that spawned this task. Use this " +
106
+ "to report progress, ask a question, or alert the parent to something. The " +
107
+ "message is delivered to the parent's input queue and picked up on its next " +
108
+ "sync cycle. Only works when this agent was spawned by another agent (i.e. " +
109
+ "it has a parent taskId).",
110
+ parameters: {
111
+ type: "object",
112
+ positional: true,
113
+ properties: {
114
+ message: {
115
+ type: "string",
116
+ description: "The message to send back to the parent agent",
117
+ },
118
+ },
119
+ required: ["message"],
120
+ },
121
+ },
122
+ };
@@ -0,0 +1,209 @@
1
+ import { Tool } from "../../clients/types";
2
+ import { GenerationSource } from "../../types";
3
+ import { generate, GenerateOptions, buildWaves } from "../../generate";
4
+
5
+ export interface RunGenerateParams {
6
+ /**
7
+ * In-memory generation sources defining the pipeline. When provided, these
8
+ * are used INSTEAD of the sources in .knowhow/knowhow.json. Each source can
9
+ * declare `dependsOn` for explicit ordering, an `agent` for agent-driven
10
+ * generation, and input/output/prompt for the work itself.
11
+ */
12
+ sources?: GenerationSource[];
13
+ /** Only run sources whose `name` matches exactly this value (config mode). */
14
+ name?: string;
15
+ /** Only run sources whose name/input/output contains this substring. */
16
+ filter?: string;
17
+ /** Skip hash checks and regenerate all matching sources regardless of changes. */
18
+ force?: boolean;
19
+ /** Max sources to run in parallel within a dependency wave. Default: 3. */
20
+ concurrency?: number;
21
+ /**
22
+ * When true, agent-driven sources create .knowhow/processes/agents/<taskId>/
23
+ * so they appear in `knowhow agents list`.
24
+ */
25
+ syncFs?: boolean;
26
+ /**
27
+ * When true, agent-driven sources push their work to a remote Knowhow task
28
+ * (identified by the source's taskId).
29
+ */
30
+ syncRemote?: boolean;
31
+ /** When true, only compute and return the execution plan (waves) without running. */
32
+ planOnly?: boolean;
33
+ /** Per-call context injected by ToolsService.callTool. */
34
+ _ctx?: { caller?: any; taskId?: string; [key: string]: any };
35
+ }
36
+
37
+ /**
38
+ * runGenerate — expose `knowhow generate` as an agent tool.
39
+ *
40
+ * This lets an agent orchestrate complex, multi-step pipelines at runtime
41
+ * without needing them defined in the config. It supports:
42
+ * - `dependsOn` for explicit ordering (topological waves)
43
+ * - automatic I/O overlap detection (source B reading source A's output runs after A)
44
+ * - per-source `agent` for agent-driven generation (map/reduce fan-out)
45
+ * - concurrency control within each dependency wave
46
+ * - plain summarization/analysis sources (no agent)
47
+ *
48
+ * With `planOnly: true`, it returns the computed wave plan so the agent can
49
+ * inspect ordering/parallelism before committing to a run.
50
+ */
51
+ export async function runGenerate(params: RunGenerateParams): Promise<string> {
52
+ const {
53
+ sources,
54
+ name,
55
+ filter,
56
+ force,
57
+ concurrency,
58
+ syncFs,
59
+ syncRemote,
60
+ planOnly,
61
+ } = params ?? {};
62
+
63
+ // Default filesystem sync ON for agent-driven sources so they show up in
64
+ // `knowhow agents list` and can be attached/tailed. Pass syncFs: false to opt out.
65
+ const effectiveSyncFs = syncFs !== false;
66
+
67
+ const options: GenerateOptions = {
68
+ sources,
69
+ name,
70
+ filter,
71
+ force,
72
+ concurrency,
73
+ syncFs: effectiveSyncFs,
74
+ syncRemote,
75
+ };
76
+
77
+ // Resolve the source set for planning output.
78
+ const effectiveSources = sources ?? [];
79
+
80
+ if (planOnly) {
81
+ if (effectiveSources.length === 0) {
82
+ return (
83
+ "planOnly requires in-memory `sources` to build a plan. " +
84
+ "Provide sources to see how they'd be grouped into dependency waves."
85
+ );
86
+ }
87
+ const filtered = effectiveSources.filter((s) => {
88
+ if (name) return s.name === name;
89
+ if (filter) {
90
+ const needle = filter.toLowerCase();
91
+ return [s.name, s.input, s.output]
92
+ .filter(Boolean)
93
+ .some((v) => v!.toLowerCase().includes(needle));
94
+ }
95
+ return true;
96
+ });
97
+ const waves = buildWaves(filtered);
98
+ const lines: string[] = [];
99
+ lines.push(
100
+ `📋 Generation Plan (concurrency: ${concurrency ?? 3}, ${filtered.length} sources, ${waves.length} wave${waves.length !== 1 ? "s" : ""})`
101
+ );
102
+ waves.forEach((wave, i) => {
103
+ lines.push(`\n┌─ Wave ${i + 1}/${waves.length} (${wave.length} in parallel)`);
104
+ for (const s of wave) {
105
+ const label = s.name || "(unnamed)";
106
+ const io = `${s.input || "-"} → ${s.output || "-"}`;
107
+ const agentPart = s.agent ? ` [agent: ${s.agent}]` : "";
108
+ lines.push(`│ • ${label} ${io}${agentPart}`);
109
+ }
110
+ lines.push("└────────────────────────────────────────────────");
111
+ });
112
+ return lines.join("\n");
113
+ }
114
+
115
+ try {
116
+ await generate(options);
117
+ } catch (e: any) {
118
+ return `Generation failed: ${e?.message ?? String(e)}`;
119
+ }
120
+
121
+ const count = effectiveSources.length;
122
+ const mode = count > 0 ? `${count} in-memory source(s)` : "config sources";
123
+ return (
124
+ `Generation complete for ${mode}` +
125
+ (name ? ` (name="${name}")` : filter ? ` (filter="${filter}")` : "") +
126
+ `. Outputs written to their configured paths. ` +
127
+ `If agent-driven sources used syncFs, inspect them with \`knowhow agents list\`.`
128
+ );
129
+ }
130
+
131
+ export const runGenerateDefinition: Tool = {
132
+ type: "function",
133
+ function: {
134
+ name: "runGenerate",
135
+ description:
136
+ "Run the `knowhow generate` pipeline. Accepts in-memory `sources` so you can define arbitrary " +
137
+ "task orchestration at runtime WITHOUT editing the config file. Supports dependency ordering via " +
138
+ "`dependsOn`, automatic I/O overlap detection (a source reading another's output runs after it), " +
139
+ "per-source `agent` for agent-driven generation (map/reduce fan-out — one agent per input), " +
140
+ "concurrency control within each dependency wave, and plain summarization/analysis sources. " +
141
+ "Use `planOnly: true` to preview how sources group into concurrent dependency waves before running. " +
142
+ "When `sources` is omitted, it runs the sources defined in .knowhow/knowhow.json (optionally scoped by name/filter).",
143
+ parameters: {
144
+ type: "object",
145
+ properties: {
146
+ sources: {
147
+ type: "array",
148
+ description:
149
+ "In-memory generation sources. Each item: { name?, input, output, prompt, agent?, model?, " +
150
+ "kind?, outputExt?, outputName?, dependsOn?: string[], syncFs?, maxTimeLimit?, maxSpendLimit? }. " +
151
+ "`input` is a glob or comma-separated file list; `output` is the target file/dir; `prompt` is the " +
152
+ "generation instruction; `agent` (optional) runs an agent per input instead of a summarization call; " +
153
+ "`dependsOn` lists names of sources that must finish first.",
154
+ items: {
155
+ type: "object",
156
+ properties: {
157
+ name: { type: "string" },
158
+ input: { type: "string" },
159
+ output: { type: "string" },
160
+ prompt: { type: "string" },
161
+ agent: { type: "string" },
162
+ model: { type: "string" },
163
+ kind: { type: "string" },
164
+ outputExt: { type: "string" },
165
+ outputName: { type: "string" },
166
+ dependsOn: { type: "array", items: { type: "string" } },
167
+ syncFs: { type: "boolean" },
168
+ maxTimeLimit: { type: "number" },
169
+ maxSpendLimit: { type: "number" },
170
+ },
171
+ },
172
+ },
173
+ name: {
174
+ type: "string",
175
+ description: "Only run sources whose `name` matches exactly (config mode).",
176
+ },
177
+ filter: {
178
+ type: "string",
179
+ description: "Only run sources whose name/input/output contains this substring.",
180
+ },
181
+ force: {
182
+ type: "boolean",
183
+ description: "Skip hash checks and regenerate all matching sources.",
184
+ },
185
+ concurrency: {
186
+ type: "number",
187
+ description: "Max sources to run in parallel within a dependency wave. Default: 3.",
188
+ },
189
+ syncFs: {
190
+ type: "boolean",
191
+ description:
192
+ "For agent-driven sources, create .knowhow/processes/agents/<taskId>/ so they appear in `knowhow agents list`. " +
193
+ "Defaults to true (enabled). Pass false to opt out.",
194
+ },
195
+ syncRemote: {
196
+ type: "boolean",
197
+ description:
198
+ "For agent-driven sources, push their work to a remote Knowhow task (identified by the source's taskId), " +
199
+ "in addition to fs sync.",
200
+ },
201
+ planOnly: {
202
+ type: "boolean",
203
+ description: "Return the wave execution plan without running anything (requires `sources`).",
204
+ },
205
+ },
206
+ required: [],
207
+ },
208
+ },
209
+ };
@@ -0,0 +1,138 @@
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
+ * Append a line to a task's input.txt without clobbering other pending
13
+ * messages. AgentSyncFs.checkForChanges reads input.txt and turns non-slash
14
+ * content into a pending user message on the target agent (and honors /poke to
15
+ * interrupt).
16
+ */
17
+ function appendToInput(inputPath: string, line: string): void {
18
+ const existing = fs.existsSync(inputPath)
19
+ ? fs.readFileSync(inputPath, "utf8")
20
+ : "";
21
+ const next =
22
+ existing && !existing.endsWith("\n")
23
+ ? `${existing}\n${line}\n`
24
+ : `${existing}${line}\n`;
25
+ fs.writeFileSync(inputPath, next, "utf8");
26
+ }
27
+
28
+ export interface SendAgentMessageParams {
29
+ /** The taskId of the agent to send a message to. */
30
+ taskId: string;
31
+ /** The message body to deliver. */
32
+ message: string;
33
+ /**
34
+ * Optional sender identifier. If omitted, resolved from the calling agent's
35
+ * own taskId via _ctx. Included as a `[from:...]` envelope so the recipient
36
+ * knows who to reply to.
37
+ */
38
+ from?: string;
39
+ /**
40
+ * When true, deliver as a `/poke` (interrupts the recipient's current step)
41
+ * rather than a queued user message. Default false.
42
+ */
43
+ poke?: boolean;
44
+ _ctx?: ToolCallContext;
45
+ }
46
+
47
+ /**
48
+ * Send a message to another running agent (a child, a peer, or the parent) by
49
+ * appending it to that agent's input.txt. This is the "down/lateral edge" of
50
+ * agent-to-agent communication — the complement to replyToParent (the up edge).
51
+ *
52
+ * The recipient's AgentSyncFs picks up the message on its next sync cycle and
53
+ * adds it as a pending user message. If `poke` is true, it is delivered as a
54
+ * `/poke` command which interrupts the recipient's current step.
55
+ *
56
+ * Self-referential: the sender identity is resolved from `_ctx` (the calling
57
+ * agent) when `from` isn't supplied.
58
+ */
59
+ export async function sendAgentMessage(
60
+ params: SendAgentMessageParams
61
+ ): Promise<string> {
62
+ const { taskId, message, from, poke = false, _ctx } = params;
63
+
64
+ if (!taskId) {
65
+ return "sendAgentMessage: taskId is required.";
66
+ }
67
+ if (!message || !message.trim()) {
68
+ return "sendAgentMessage: message is required.";
69
+ }
70
+
71
+ const targetDir = path.join(agentsDir(), taskId);
72
+ const inputPath = path.join(targetDir, "input.txt");
73
+
74
+ if (!fs.existsSync(targetDir)) {
75
+ return `sendAgentMessage: target task directory not found: ${targetDir}`;
76
+ }
77
+
78
+ const fromId =
79
+ from ??
80
+ _ctx?.taskId ??
81
+ (_ctx?.caller as any)?.currentTaskId ??
82
+ "unknown";
83
+ const fromAgent = (_ctx?.caller as any)?.name;
84
+ const fromLabel = fromAgent ? `${fromId}/${fromAgent}` : fromId;
85
+
86
+ try {
87
+ if (poke) {
88
+ // /poke [message] — interrupts the recipient's current step.
89
+ appendToInput(inputPath, `/poke [from:${fromLabel}] ${message}`);
90
+ } else {
91
+ appendToInput(inputPath, `[from:${fromLabel}] ${message}`);
92
+ }
93
+ } catch (e: any) {
94
+ return `sendAgentMessage: failed to write to ${inputPath}: ${
95
+ e?.message ?? String(e)
96
+ }`;
97
+ }
98
+
99
+ return `Sent message to task ${taskId}${poke ? " (as /poke)" : ""}.`;
100
+ }
101
+
102
+ export const sendAgentMessageDefinition: Tool = {
103
+ type: "function",
104
+ function: {
105
+ name: "sendAgentMessage",
106
+ description:
107
+ "Send a message to another running agent (a child you spawned, a peer, or your parent) by " +
108
+ "appending it to that agent's input queue. The recipient picks it up on its next sync cycle " +
109
+ "as a user message. This is the down/lateral communication edge (complement to replyToParent). " +
110
+ "Set poke:true to interrupt the recipient's current step (delivered as /poke). Your sender " +
111
+ "identity is attached automatically so the recipient can reply back to you.",
112
+ parameters: {
113
+ type: "object",
114
+ positional: false,
115
+ properties: {
116
+ taskId: {
117
+ type: "string",
118
+ description: "The taskId of the agent to send the message to.",
119
+ },
120
+ message: {
121
+ type: "string",
122
+ description: "The message to deliver to the recipient agent.",
123
+ },
124
+ from: {
125
+ type: "string",
126
+ description:
127
+ "Optional sender id. Defaults to the calling agent's own taskId.",
128
+ },
129
+ poke: {
130
+ type: "boolean",
131
+ description:
132
+ "When true, deliver as a /poke that interrupts the recipient's current step. Default false.",
133
+ },
134
+ },
135
+ required: ["taskId", "message"],
136
+ },
137
+ },
138
+ };
@@ -9,6 +9,11 @@ export interface StartAgentTaskParams {
9
9
  taskId?: string;
10
10
  resume?: boolean;
11
11
  prompt: string;
12
+ /**
13
+ * Push this agent's work to a remote Knowhow task identified by taskId.
14
+ * When set (and no messageId), the spawned agent attaches to that remote task.
15
+ */
16
+ syncRemote?: boolean;
12
17
  provider?: string;
13
18
  model?: string;
14
19
  agentName?: string;
@@ -16,6 +21,15 @@ export interface StartAgentTaskParams {
16
21
  maxSpendLimit?: number;
17
22
  /** When true, wait for the agent subprocess to exit before resolving (for generate pipelines). */
18
23
  waitForCompletion?: boolean;
24
+ /**
25
+ * The taskId of the parent agent that is spawning this task. Passed to the
26
+ * child via --parent-task-id so the child knows who to report back to. When
27
+ * omitted, it is auto-populated from the per-call context (`_ctx.taskId`) of
28
+ * the spawning agent.
29
+ */
30
+ parentTaskId?: string;
31
+ /** Per-call context injected by ToolsService.callTool (contains caller + taskId). */
32
+ _ctx?: { caller?: any; taskId?: string; [key: string]: any };
19
33
  }
20
34
 
21
35
  const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
@@ -39,6 +53,37 @@ function generateTaskId(prompt: string): string {
39
53
  return fullId.slice(0, 80);
40
54
  }
41
55
 
56
+ /**
57
+ * A short capabilities note appended to a spawned subagent's prompt so it knows
58
+ * it can use the `knowhow agents` and related CLI commands (via its execCommand
59
+ * tool) to coordinate, inspect, and orchestrate other agents — things it may
60
+ * not have dedicated tools for.
61
+ */
62
+ function subagentCapabilitiesNote(): string {
63
+ const lines = [
64
+ "",
65
+ "---",
66
+ "SUBAGENT ORCHESTRATION CAPABILITIES:",
67
+ "You are running as a knowhow agent with a synchronized task directory. In addition to your tools,",
68
+ "you can shell out (via execCommand) to the `knowhow agents` CLI to coordinate and inspect other agents:",
69
+ " • knowhow agents list — list running/recent agent tasks (with row indexes)",
70
+ " • knowhow agents status <id|-i N> — status, cost, last tool/message for a task",
71
+ " • knowhow agents tail <id|-i N> [-f] — read recent messages (read-only; -f to follow live)",
72
+ " • knowhow agents answer <id|-i N> — print the final answer of a COMPLETED task",
73
+ " • knowhow agents attach <id|-i N> — open the interactive chat attached to a running task",
74
+ "You can spawn more subagents with the startAgentTask tool, or run whole pipelines with the runGenerate tool",
75
+ "(supports dependsOn ordering, per-source agents, and concurrency for map/reduce fan-out).",
76
+ "To coordinate spawned subagents from your tools: use waitForAgentCompleted (join a subagent and",
77
+ "get a structured { status, costUsd, finalAnswer } result) and observe (stream a tool's results",
78
+ "back to yourself on an interval, e.g. observe waitForAgentCompleted or `agents status`).",
79
+ "To communicate with running agents: use sendAgentMessage (send a message or /poke to a child/peer/parent),",
80
+ "replyToParent (report back to whoever spawned you), and connectAgent (wire agents together with an ARRAY",
81
+ "of { listener, speaker } connections — enables bidirectional, pipeline, star, and mesh topologies in one call).",
82
+ ];
83
+ lines.push("---", "");
84
+ return lines.join("\n");
85
+ }
86
+
42
87
  /**
43
88
  * Creates a chat task in Knowhow based on a message ID and prompt.
44
89
  * Spawns the knowhow CLI with the prompt piped via stdin to avoid
@@ -60,31 +105,50 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
60
105
  taskId: providedTaskId,
61
106
  resume,
62
107
  syncFs,
108
+ syncRemote,
63
109
  provider,
64
110
  model,
65
111
  agentName,
66
112
  maxTimeLimit,
67
113
  maxSpendLimit,
68
114
  waitForCompletion,
115
+ parentTaskId: explicitParentTaskId,
116
+ _ctx,
69
117
  } = params;
70
118
  if (!prompt) {
71
119
  throw new Error("prompt is required to create a chat task");
72
120
  }
73
121
 
122
+ // Default filesystem synchronization ON unless the caller explicitly opts out
123
+ // (syncFs: false) or is using a messageId-based web sync. This ensures spawned
124
+ // subagents always appear in `knowhow agents list` and can be attached/tailed.
125
+ const useSyncFs = syncFs !== false && !messageId;
126
+
74
127
  // Use provided taskId if given, otherwise generate one from the prompt
75
128
  const taskId = providedTaskId ?? generateTaskId(prompt);
76
129
  const agentTaskDir = path.join(AGENTS_DIR, taskId);
77
130
 
131
+ // Determine the parent taskId: explicit param wins, otherwise fall back to
132
+ // the spawning agent's taskId carried in the per-call context (`_ctx`).
133
+ const parentTaskId =
134
+ explicitParentTaskId ?? _ctx?.taskId ?? (_ctx?.caller as any)?.currentTaskId;
135
+
78
136
  // Build args array (no shell escaping needed - args are passed directly)
79
137
  const args: string[] = ["agent"];
80
138
 
81
139
  if (messageId) {
82
140
  args.push("--message-id", messageId);
83
- } else if (syncFs) {
141
+ } else if (useSyncFs) {
84
142
  args.push("--sync-fs");
85
143
  }
144
+ // When syncRemote is requested, pass it through so the spawned agent pushes
145
+ // its work to the remote task identified by --task-id rather than staying
146
+ // local-only.
147
+ if (syncRemote) {
148
+ args.push("--sync-remote");
149
+ }
86
150
 
87
- if (syncFs || providedTaskId) {
151
+ if (useSyncFs || providedTaskId) {
88
152
  // Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
89
153
  args.push("--task-id", taskId);
90
154
  }
@@ -108,6 +172,10 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
108
172
  if (maxSpendLimit !== undefined) {
109
173
  args.push("--max-spend-limit", String(maxSpendLimit));
110
174
  }
175
+ if (parentTaskId) {
176
+ // Tell the child who spawned it so it can report back to the parent.
177
+ args.push("--parent-task-id", parentTaskId);
178
+ }
111
179
  if (resume) {
112
180
  // --resume is a boolean flag; task ID is already passed via --task-id above
113
181
  args.push("--resume");
@@ -137,7 +205,12 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
137
205
  fs.writeSync(fd, `PID: ${pid}\n`);
138
206
 
139
207
  // Write prompt to stdin and close it so the process reads it
140
- child.stdin!.write(prompt, "utf8");
208
+ // Append the subagent orchestration capabilities note (unless resuming — a
209
+ // resumed task already has its full context and we don't want to re-inject).
210
+ const promptWithCaps = resume
211
+ ? prompt
212
+ : prompt + subagentCapabilitiesNote();
213
+ child.stdin!.write(promptWithCaps, "utf8");
141
214
  child.stdin!.end();
142
215
 
143
216
  return new Promise<string>((resolve) => {
@@ -153,7 +226,7 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
153
226
  done(`Failed to start agent: ${String(e)}\nLogs: ${logPath}`);
154
227
  });
155
228
 
156
- const syncFsNote = syncFs
229
+ const syncFsNote = useSyncFs
157
230
  ? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
158
231
  `To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
159
232
  `To check status, read: ${agentTaskDir}/status.txt\n`
@@ -201,7 +274,15 @@ export const startAgentTaskDefinition: Tool = {
201
274
  syncFs: {
202
275
  type: "boolean",
203
276
  description:
204
- "Enable filesystem-based synchronization for the task. Use this when no messageId is available.",
277
+ "Filesystem-based synchronization for the task. Defaults to true (enabled) when no messageId is given, " +
278
+ "so the spawned agent always appears in `knowhow agents list` and can be attached/tailed. Pass false to opt out.",
279
+ },
280
+ syncRemote: {
281
+ type: "boolean",
282
+ description:
283
+ "Push this agent's work to a remote Knowhow task identified by taskId (in addition to fs sync). " +
284
+ "Use this to sync a spawned subagent's progress to an already-created remote task. " +
285
+ "If messageId is given instead, a fresh remote task is created from that message.",
205
286
  },
206
287
  prompt: {
207
288
  type: "string",
@@ -238,6 +319,12 @@ export const startAgentTaskDefinition: Tool = {
238
319
  description:
239
320
  "Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
240
321
  },
322
+ parentTaskId: {
323
+ type: "string",
324
+ description:
325
+ "The taskId of the parent that should be notified by this child (e.g. via replyToParent). " +
326
+ "If omitted, it is auto-populated from the spawning agent's own taskId.",
327
+ },
241
328
  },
242
329
  required: ["prompt"],
243
330
  },