@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,269 @@
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
+ const TERMINAL_STATUSES = new Set([
12
+ "completed",
13
+ "failed",
14
+ "killed",
15
+ "error",
16
+ "done",
17
+ ]);
18
+
19
+ interface AgentTaskResult {
20
+ taskId: string;
21
+ status: string;
22
+ costUsd: number;
23
+ finalAnswer?: string;
24
+ artifacts?: string[];
25
+ elapsedMs?: number;
26
+ timedOut?: boolean;
27
+ error?: string;
28
+ }
29
+
30
+ function readMeta(taskId: string): any | null {
31
+ try {
32
+ const metaPath = path.join(agentsDir(), taskId, "metadata.json");
33
+ if (!fs.existsSync(metaPath)) return null;
34
+ return JSON.parse(fs.readFileSync(metaPath, "utf8"));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function readStatusFile(taskId: string): string | null {
41
+ try {
42
+ const statusPath = path.join(agentsDir(), taskId, "status.txt");
43
+ if (!fs.existsSync(statusPath)) return null;
44
+ return fs.readFileSync(statusPath, "utf8").trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Consolidated status read. metadata.status is the live/canonical value; the
52
+ * status.txt file is a fallback signal. If metadata says the task is no longer
53
+ * inProgress but has no explicit terminal status, treat it as completed.
54
+ */
55
+ function resolveStatus(meta: any | null, statusFile: string | null): string {
56
+ if (meta?.status) return meta.status;
57
+ if (statusFile) return statusFile;
58
+ if (meta && meta.inProgress === false) return "completed";
59
+ return "unknown";
60
+ }
61
+
62
+ /** Extract the last assistant message from a thread as a fallback finalAnswer. */
63
+ function lastAssistantText(threads: any): string | undefined {
64
+ if (!threads) return undefined;
65
+ // threads may be an array of message arrays, or a flat message array.
66
+ let messages: any[] = [];
67
+ if (Array.isArray(threads)) {
68
+ if (threads.length && Array.isArray(threads[0])) {
69
+ // array of threads — flatten
70
+ messages = threads.flat();
71
+ } else {
72
+ messages = threads;
73
+ }
74
+ }
75
+ for (let i = messages.length - 1; i >= 0; i--) {
76
+ const msg = messages[i];
77
+ if (msg?.role === "assistant") {
78
+ if (typeof msg.content === "string" && msg.content.trim()) {
79
+ return msg.content.trim();
80
+ }
81
+ if (Array.isArray(msg.content)) {
82
+ const text = msg.content
83
+ .filter((c: any) => c?.type === "text")
84
+ .map((c: any) => c.text)
85
+ .join("\n")
86
+ .trim();
87
+ if (text) return text;
88
+ }
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /**
95
+ * Check whether a process id is still alive. Used as a fallback liveness check
96
+ * so we don't wait forever on a task whose process crashed without writing a
97
+ * terminal status.
98
+ */
99
+ function pidAlive(pid: number): boolean {
100
+ try {
101
+ // signal 0 doesn't kill — it only checks for existence/permission.
102
+ process.kill(pid, 0);
103
+ return true;
104
+ } catch (e: any) {
105
+ // ESRCH = no such process. EPERM = exists but not ours (still alive).
106
+ return e?.code === "EPERM";
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Discover the pid of a spawned agent task. AgentSyncFs.createTask records the
112
+ * agent subprocess's own `process.pid` into metadata.json, so we read the
113
+ * `pid` (or legacy `processId`) field from there. If it's absent (e.g. an older
114
+ * task created before this was persisted), we skip the liveness check.
115
+ */
116
+ function metaPid(meta: any | null): number | undefined {
117
+ const pid = meta?.pid ?? meta?.processId;
118
+ return typeof pid === "number" ? pid : undefined;
119
+ }
120
+
121
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
122
+
123
+ export interface WaitForAgentCompletedParams {
124
+ /** The taskId of the agent to wait for. */
125
+ taskId: string;
126
+ /** Max time to wait in milliseconds before returning timedOut. Default 30 min. */
127
+ timeoutMs?: number;
128
+ /** How often to poll status in milliseconds. Default 2000ms. */
129
+ pollIntervalMs?: number;
130
+ _ctx?: ToolCallContext;
131
+ }
132
+
133
+ /**
134
+ * Block until a spawned agent task reaches a terminal status, then return a
135
+ * structured result: { status, costUsd, finalAnswer, artifacts }.
136
+ *
137
+ * This is the native "join" primitive for orchestration — it must be a real
138
+ * tool (not a script polling loop) because the script sandbox caps `sleep` and
139
+ * burns a tool call per poll. It reads the consolidated status from
140
+ * metadata.json / status.txt and falls back to a process-liveness check so a
141
+ * crashed child doesn't hang the parent forever.
142
+ */
143
+ export async function waitForAgentCompleted(
144
+ params: WaitForAgentCompletedParams
145
+ ): Promise<string> {
146
+ const {
147
+ taskId,
148
+ timeoutMs = 30 * 60 * 1000,
149
+ pollIntervalMs = 2000,
150
+ } = params;
151
+
152
+ if (!taskId) {
153
+ return JSON.stringify({
154
+ taskId,
155
+ status: "error",
156
+ costUsd: 0,
157
+ error: "taskId is required for waitForAgentCompleted",
158
+ } as AgentTaskResult);
159
+ }
160
+
161
+ const taskDir = path.join(agentsDir(), taskId);
162
+ if (!fs.existsSync(taskDir)) {
163
+ return JSON.stringify({
164
+ taskId,
165
+ status: "error",
166
+ costUsd: 0,
167
+ error: `No agent task directory found for taskId: ${taskId}`,
168
+ } as AgentTaskResult);
169
+ }
170
+
171
+ const start = Date.now();
172
+ const interval = Math.max(250, pollIntervalMs);
173
+ let missingProcessPolls = 0;
174
+
175
+ while (true) {
176
+ const meta = readMeta(taskId);
177
+ const statusFile = readStatusFile(taskId);
178
+ const status = resolveStatus(meta, statusFile);
179
+
180
+ if (TERMINAL_STATUSES.has(status)) {
181
+ const finalAnswer =
182
+ meta?.result ?? lastAssistantText(meta?.threads);
183
+ const result: AgentTaskResult = {
184
+ taskId,
185
+ status,
186
+ costUsd: meta?.totalCostUsd ?? 0,
187
+ finalAnswer,
188
+ artifacts: meta?.artifacts,
189
+ elapsedMs: Date.now() - start,
190
+ };
191
+ return JSON.stringify(result);
192
+ }
193
+
194
+ // Liveness fallback: if metadata exposes a pid and it is no longer alive,
195
+ // but no terminal status was written, treat the task as failed after a
196
+ // couple of confirming polls (avoids a race where the process just exited
197
+ // but hasn't flushed its final status yet).
198
+ const pid = metaPid(meta);
199
+ if (pid !== undefined && !pidAlive(pid)) {
200
+ missingProcessPolls++;
201
+ if (missingProcessPolls >= 2) {
202
+ const finalAnswer =
203
+ meta?.result ?? lastAssistantText(meta?.threads);
204
+ return JSON.stringify({
205
+ taskId,
206
+ status: meta?.result ? "completed" : "failed",
207
+ costUsd: meta?.totalCostUsd ?? 0,
208
+ finalAnswer,
209
+ artifacts: meta?.artifacts,
210
+ elapsedMs: Date.now() - start,
211
+ error: meta?.result
212
+ ? undefined
213
+ : "Agent process exited without writing a terminal status",
214
+ } as AgentTaskResult);
215
+ }
216
+ } else {
217
+ missingProcessPolls = 0;
218
+ }
219
+
220
+ if (Date.now() - start >= timeoutMs) {
221
+ return JSON.stringify({
222
+ taskId,
223
+ status,
224
+ costUsd: meta?.totalCostUsd ?? 0,
225
+ finalAnswer: meta?.result ?? lastAssistantText(meta?.threads),
226
+ artifacts: meta?.artifacts,
227
+ elapsedMs: Date.now() - start,
228
+ timedOut: true,
229
+ } as AgentTaskResult);
230
+ }
231
+
232
+ await sleep(interval);
233
+ }
234
+ }
235
+
236
+ export const waitForAgentCompletedDefinition: Tool = {
237
+ type: "function",
238
+ function: {
239
+ name: "waitForAgentCompleted",
240
+ description:
241
+ "Block until a spawned agent task (started via startAgentTask or runGenerate) reaches a " +
242
+ "terminal status (completed/failed/killed), then return a structured JSON result with " +
243
+ "{ status, costUsd, finalAnswer, artifacts, elapsedMs }. This is the native way to 'join' a " +
244
+ "subagent — prefer it over shell polling loops with sleep. Returns immediately if the task is " +
245
+ "already finished, and gives up with timedOut:true after timeoutMs.",
246
+ parameters: {
247
+ type: "object",
248
+ positional: false,
249
+ properties: {
250
+ taskId: {
251
+ type: "string",
252
+ description:
253
+ "The taskId of the agent task to wait for (as returned/used by startAgentTask).",
254
+ },
255
+ timeoutMs: {
256
+ type: "number",
257
+ description:
258
+ "Maximum time to wait in milliseconds before returning with timedOut:true. Default: 1800000 (30 min).",
259
+ },
260
+ pollIntervalMs: {
261
+ type: "number",
262
+ description:
263
+ "How often to poll the task status in milliseconds. Default: 2000.",
264
+ },
265
+ },
266
+ required: ["taskId"],
267
+ },
268
+ },
269
+ };
package/src/ai.ts CHANGED
@@ -4,6 +4,7 @@ import { readFile } from "./utils";
4
4
  /** Options forwarded to startAgentTask when a generation source has an `agent` field */
5
5
  export type AgentOptions = {
6
6
  syncFs?: boolean;
7
+ syncRemote?: boolean;
7
8
  taskId?: string;
8
9
  maxTimeLimit?: number;
9
10
  maxSpendLimit?: number;
@@ -143,6 +143,32 @@ export class AgentModule extends BaseChatModule {
143
143
  }
144
144
  },
145
145
  },
146
+ {
147
+ name: "compact",
148
+ description:
149
+ "Force the attached agent to compact (summarize) its conversation now, dropping back to a much cheaper context size",
150
+ modes: ["agent:attached"],
151
+ handler: async (_args: string[]): Promise<void> => {
152
+ if (!this.attachedAgent) {
153
+ console.log("No attached agent to compact.");
154
+ return;
155
+ }
156
+ if (typeof this.attachedAgent.requestCompact !== "function") {
157
+ console.log(
158
+ "This agent does not support manual compaction (remote/watcher-backed agents compact automatically)."
159
+ );
160
+ return;
161
+ }
162
+ this.attachedAgent.requestCompact();
163
+ // Do NOT interrupt — compaction should not cancel the in-progress
164
+ // tool call / AI completion. The _forceCompact flag is picked up at
165
+ // the compression check on the next loop iteration, once the current
166
+ // step finishes naturally.
167
+ console.log(
168
+ "🗜 Compaction requested — the agent will summarize and shrink its context on the next step."
169
+ );
170
+ },
171
+ },
146
172
  {
147
173
  name: "reasoning_effort",
148
174
  description:
@@ -654,7 +680,6 @@ export class AgentModule extends BaseChatModule {
654
680
  agentName,
655
681
  input: resumePrompt,
656
682
  messageId: session.knowhowMessageId,
657
- existingKnowhowTaskId: session.knowhowTaskId,
658
683
  chatHistory: [],
659
684
  run: false, // Don't run yet, we need to set up event listeners first
660
685
  });
@@ -724,7 +749,7 @@ export class AgentModule extends BaseChatModule {
724
749
  input: string;
725
750
  messageId?: string;
726
751
  syncFs?: boolean;
727
- existingKnowhowTaskId?: string;
752
+ syncRemote?: boolean;
728
753
  provider?: string;
729
754
  model?: string;
730
755
  maxTimeLimit?: number; // in minutes
@@ -732,6 +757,7 @@ export class AgentModule extends BaseChatModule {
732
757
  chatHistory?: ChatInteraction[];
733
758
  run?: boolean; // whether to run immediately
734
759
  taskId?: string; // optional pre-generated taskId
760
+ parentTaskId?: string; // taskId of the parent agent that spawned this task
735
761
  }) {
736
762
  const { input, chatHistory = [], agentName } = options;
737
763
  const agentContext = services().Agents.getAgentContext();
@@ -773,7 +799,9 @@ export class AgentModule extends BaseChatModule {
773
799
  let taskInfo: TaskInfo = {
774
800
  taskId,
775
801
  knowhowMessageId: options.messageId,
776
- knowhowTaskId: options.existingKnowhowTaskId, // Use existing or will be set after creating chat task
802
+ // When pushing to a remote task via --sync-remote, the taskId IS the
803
+ // remote task ID. Otherwise it will be set after creating a chat task.
804
+ knowhowTaskId: options.syncRemote ? taskId : undefined,
777
805
  agentName,
778
806
  agent,
779
807
  initialInput: input,
@@ -799,8 +827,9 @@ export class AgentModule extends BaseChatModule {
799
827
  prompt: input,
800
828
  messageId: options.messageId,
801
829
  syncFs: options.syncFs,
802
- existingKnowhowTaskId: options.existingKnowhowTaskId,
830
+ syncRemote: options.syncRemote,
803
831
  agentName,
832
+ parentTaskId: options.parentTaskId,
804
833
  });
805
834
 
806
835
  const webTaskId = syncer.getCreatedWebTaskId();
@@ -947,33 +976,27 @@ export class AgentModule extends BaseChatModule {
947
976
  role: "assistant",
948
977
  });
949
978
  };
950
- if (agent.eventTypes.toolCall) {
951
- agent.agentEvents.on(agent.eventTypes.toolCall, toolCallHandler);
952
- }
953
- if (agent.eventTypes.toolUsed) {
954
- agent.agentEvents.on(agent.eventTypes.toolUsed, toolResultHandler);
955
- }
956
- if (agent.eventTypes.agentSay) {
957
- agent.agentEvents.on(agent.eventTypes.agentSay, agentSayHandler);
979
+ // Only register these inline render listeners for the direct CLI path
980
+ // (options.run === true). The interactive chat / resume / attach paths
981
+ // call attachedAgentChatLoop -> wireAgentRendering, which registers its
982
+ // own keyed render listeners. Registering both would render every
983
+ // toolCall / toolResult / agentMessage twice.
984
+ if (options.run) {
985
+ if (agent.eventTypes.toolCall) {
986
+ agent.agentEvents.on(agent.eventTypes.toolCall, toolCallHandler);
987
+ }
988
+ if (agent.eventTypes.toolUsed) {
989
+ agent.agentEvents.on(agent.eventTypes.toolUsed, toolResultHandler);
990
+ }
991
+ if (agent.eventTypes.agentSay) {
992
+ agent.agentEvents.on(agent.eventTypes.agentSay, agentSayHandler);
993
+ }
958
994
  }
959
995
  // Track the active task on the renderer so it doesn't filter out events
960
996
  this.renderer.setActiveTaskId(taskId);
961
997
  this.activeAgentTaskId = taskId;
962
998
 
963
999
  const taskCompleted = new Promise<string>((resolve) => {
964
- // Remove tool rendering listeners
965
- if (agent.eventTypes.toolCall) {
966
- agent.agentEvents.removeListener(agent.eventTypes.toolCall, toolCallHandler);
967
- }
968
- if (agent.eventTypes.toolUsed) {
969
- agent.agentEvents.removeListener(agent.eventTypes.toolUsed, toolResultHandler);
970
- }
971
- if (agent.eventTypes.agentSay) {
972
- agent.agentEvents.removeListener(agent.eventTypes.agentSay, agentSayHandler);
973
- }
974
- // Clear active task on renderer
975
- this.renderer.setActiveTaskId(undefined);
976
- this.activeAgentTaskId = undefined;
977
1000
  agent.agentEvents.once(agent.eventTypes.done, async (doneMsg) => {
978
1001
  console.log("🎯 [AgentModule] Task Completed");
979
1002
  done = true;
@@ -994,6 +1017,19 @@ export class AgentModule extends BaseChatModule {
994
1017
  );
995
1018
  // Update task info
996
1019
  taskInfo = this.taskRegistry.get(taskId);
1020
+ // Remove tool rendering listeners
1021
+ if (agent.eventTypes.toolCall) {
1022
+ agent.agentEvents.removeListener(agent.eventTypes.toolCall, toolCallHandler);
1023
+ }
1024
+ if (agent.eventTypes.toolUsed) {
1025
+ agent.agentEvents.removeListener(agent.eventTypes.toolUsed, toolResultHandler);
1026
+ }
1027
+ if (agent.eventTypes.agentSay) {
1028
+ agent.agentEvents.removeListener(agent.eventTypes.agentSay, agentSayHandler);
1029
+ }
1030
+ // Clear active task on renderer
1031
+ this.renderer.setActiveTaskId(undefined);
1032
+ this.activeAgentTaskId = undefined;
997
1033
 
998
1034
  // Wait for AgentSync to finish before resolving
999
1035
  await syncer.waitForFinalization();
@@ -1092,6 +1128,7 @@ export class AgentModule extends BaseChatModule {
1092
1128
  threads: Message[][];
1093
1129
  messageId?: string;
1094
1130
  taskId?: string;
1131
+ interactive?: boolean;
1095
1132
  }): Promise<{ taskCompleted: Promise<string> }> {
1096
1133
  const { agentName, input, threads, messageId, taskId } = options;
1097
1134
 
@@ -1147,11 +1184,36 @@ export class AgentModule extends BaseChatModule {
1147
1184
  agentName,
1148
1185
  input: resumePrompt,
1149
1186
  messageId,
1150
- existingKnowhowTaskId: taskId,
1187
+ taskId,
1188
+ // When resuming a remote task (no messageId), keep pushing work to the
1189
+ // remote task identified by taskId.
1190
+ syncRemote: !messageId && !!taskId,
1151
1191
  run: false,
1152
1192
  });
1153
1193
 
1154
- // Start agent with prior messages as context
1194
+ // Interactive (chat REPL) resume: route through the attached chat loop so the
1195
+ // renderer is wired the same way as a normal chat interaction / /attach, and
1196
+ // the user gets agent:attached mode commands (/poke, /detach, /kill, /logs).
1197
+ if (options.interactive) {
1198
+ await this.attachedAgentChatLoop(
1199
+ result.taskId,
1200
+ result.agent,
1201
+ resumePrompt,
1202
+ resumeMessages
1203
+ );
1204
+ return { taskCompleted: result.taskCompleted };
1205
+ }
1206
+
1207
+ // Non-interactive (headless CLI `knowhow agent --resume`) path: setupAgent was
1208
+ // called with run:false, so no inline render listeners were registered. Wire
1209
+ // rendering here (same keyed listeners as attachedAgentChatLoop) so the CLI
1210
+ // still shows the agent's messages / tool calls, then start the agent.
1211
+ this.wireAgentRendering(
1212
+ result.taskId,
1213
+ result.agent.agentEvents,
1214
+ result.agent.eventTypes,
1215
+ result.agent.name
1216
+ );
1155
1217
  result.agent.call(resumePrompt, resumeMessages);
1156
1218
 
1157
1219
  return { taskCompleted: result.taskCompleted };
@@ -395,6 +395,7 @@ export class SessionsModule extends BaseChatModule {
395
395
  taskId: id,
396
396
  threads: normalizedThreads,
397
397
  input: additionalContext?.trim() || initialInput || "",
398
+ interactive: true,
398
399
  });
399
400
  return;
400
401
  } catch (e: any) {
@@ -770,18 +771,15 @@ export class SessionsModule extends BaseChatModule {
770
771
  await watcher.startWatching(taskId);
771
772
  this.agentModule.setActiveSyncedWatcher(watcher);
772
773
 
773
- // Wire rendering via AgentModule utility (handles cleanup on detach)
774
- this.agentModule.wireAgentRendering(taskId, watcher.agentEvents, watcher.eventTypes, watcher.agentName);
775
- watcher.agentEvents.once(watcher.eventTypes.done, (output) => {
776
- console.log(Marked.parse(output));
777
- });
778
-
779
774
  const context = this.chatService?.getContext();
780
775
  if (context) context.activeAgentTaskId = taskId;
781
776
 
782
777
  console.log(`📁 Attached to filesystem agent: ${taskId}`);
783
778
 
784
779
  // Enter interactive loop — this sets mode to "agent:attached" and blocks until detach/done/kill
780
+ // attachedAgentChatLoop calls wireAgentRendering internally — do NOT call it here too,
781
+ // otherwise two sets of listeners are registered and output is doubled (or, after detach,
782
+ // the wrong agentEvents object holds the listeners).
785
783
  const fsWatcherAgent = new WatcherBackedAgent(watcher);
786
784
  await this.agentModule.attachedAgentChatLoop(taskId, fsWatcherAgent);
787
785
  }
@@ -811,15 +809,13 @@ export class SessionsModule extends BaseChatModule {
811
809
  await watcher.startWatching(taskId);
812
810
  this.agentModule.setActiveSyncedWatcher(watcher);
813
811
 
814
- // Wire rendering via AgentModule utility (handles cleanup on detach)
815
- this.agentModule.wireAgentRendering(taskId, watcher.agentEvents, watcher.eventTypes, watcher.agentName);
816
-
817
812
  const context = this.chatService?.getContext();
818
813
  if (context) context.activeAgentTaskId = taskId;
819
814
 
820
815
  console.log(`🌐 Attached to web agent: ${taskId}`);
821
816
 
822
- // Enter interactive loop — this sets mode to "agent:attached" and blocks until detach/done/kill
817
+ // Enter interactive loop — wireAgentRendering is called inside attachedAgentChatLoop.
818
+ // Do NOT call it here too (would double-register listeners).
823
819
  const webWatcherAgent = new WatcherBackedAgent(watcher);
824
820
  await this.agentModule.attachedAgentChatLoop(taskId, webWatcherAgent);
825
821
  }
@@ -86,6 +86,9 @@ export const ContextLimits: Record<string, number> = {
86
86
  // ─── Anthropic ────────────────────────────────────────────────────────────
87
87
  [Models.anthropic.Opus4_8Fast]: 1_000_000,
88
88
  [Models.anthropic.Opus4_8]: 1_000_000,
89
+ // Claude Opus 5 (launched 2026-06-09, 1M context window)
90
+ [Models.anthropic.Opus5]: 1_000_000,
91
+ [Models.anthropic.Opus5Fast]: 1_000_000,
89
92
  // Claude 5 generation (launched 2026-06-09, 1M context window)
90
93
  [Models.anthropic.Fable5]: 1_000_000,
91
94
  [Models.anthropic.Sonnet5]: 1_000_000,
@@ -101,6 +104,10 @@ export const ContextLimits: Record<string, number> = {
101
104
  [Models.anthropic.Sonnet4]: 200_000,
102
105
  [Models.anthropic.Sonnet4_5]: 200_000,
103
106
  [Models.anthropic.Haiku4_5]: 200_000,
107
+ // Short aliases (resolve to their latest dated version)
108
+ [Models.anthropic.Sonnet4_5Short]: 200_000,
109
+ [Models.anthropic.Haiku4_5Short]: 200_000,
110
+ [Models.anthropic.Opus4_5Short]: 1_000_000,
104
111
  [Models.anthropic.Sonnet3_7]: 200_000,
105
112
  [Models.anthropic.Sonnet3_5]: 200_000,
106
113
  [Models.anthropic.Opus3]: 200_000,
@@ -115,6 +122,20 @@ export const ContextLimits: Record<string, number> = {
115
122
  [Models.anthropic.Sonnet4_0]: 200_000,
116
123
 
117
124
  // ─── Google ───────────────────────────────────────────────────────────────
125
+ // Gemini 3.6 / 3.5 (text)
126
+ [Models.google.Gemini_36_Flash]: 1_000_000,
127
+ [Models.google.Gemini_35_Flash]: 1_000_000,
128
+ [Models.google.Gemini_35_Flash_Lite]: 1_000_000,
129
+ [Models.google.Gemini_35_Live_Translate_Preview]: 1_000_000,
130
+ // Gemini 3.x (text / multimodal reasoning)
131
+ [Models.google.Gemini_31_Flash_Lite]: 1_000_000,
132
+ [Models.google.Gemini_31_Flash_TTS_Preview]: 1_000_000,
133
+ [Models.google.Gemini_3_Pro_Preview]: 1_000_000,
134
+ [Models.google.Gemini_Omni_Flash_Preview]: 1_000_000,
135
+ [Models.google.Gemini_25_Computer_Use_Preview]: 1_000_000,
136
+ [Models.google.Gemini_20_Flash_Lite]: 1_000_000,
137
+ [Models.google.Gemini_Robotics_ER_16_Preview]: 1_000_000,
138
+ [Models.google.Gemini_Robotics_ER_15_Preview]: 1_000_000,
118
139
  [Models.google.Gemini_31_Pro_Preview]: 1_000_000,
119
140
  [Models.google.Gemini_31_Flash_Image_Preview]: 1_000_000,
120
141
  [Models.google.Gemini_31_Flash_Lite_Preview]: 1_000_000,
@@ -142,6 +163,11 @@ export const ContextLimits: Record<string, number> = {
142
163
  [Models.google.Veo_3_Fast]: 0,
143
164
  [Models.google.Veo_3_1]: 0,
144
165
  [Models.google.Veo_3_1_Fast]: 0,
166
+ [Models.google.Veo_3_1_Lite]: 0,
167
+ // Google image generation models — no text context window; use 0
168
+ [Models.google.Gemini_31_Flash_Image]: 0,
169
+ [Models.google.Gemini_31_Flash_Lite_Image]: 0,
170
+ [Models.google.Gemini_3_Pro_Image]: 0,
145
171
  [Models.google.Gemini_20_Flash]: 1_000_000,
146
172
  [Models.google.Gemini_20_Flash_Preview_Image_Generation]: 1_000_000,
147
173
  [Models.google.Gemini_20_Flash_Live]: 1_000_000,
@@ -20,6 +20,8 @@ export const AnthropicModels = {
20
20
  Sonnet5: "claude-sonnet-5",
21
21
  Mythos5: "claude-mythos-5",
22
22
  MythosPreview: "claude-mythos-preview",
23
+ Opus5: "claude-opus-5",
24
+ Opus5Fast: "claude-opus-5-fast",
23
25
  Opus4_8: "claude-opus-4-8",
24
26
  Opus4_8Fast: "claude-opus-4-8-fast",
25
27
  Opus4_7: "claude-opus-4-7",
@@ -52,6 +54,7 @@ export const AnthropicTextModels: string[] = [
52
54
  AnthropicModels.Opus4_8,
53
55
  AnthropicModels.Fable5,
54
56
  AnthropicModels.Sonnet5,
57
+ AnthropicModels.Opus5,
55
58
  AnthropicModels.Opus4_7,
56
59
  AnthropicModels.Opus4_6,
57
60
  AnthropicModels.Sonnet4_6,
@@ -64,6 +67,7 @@ export const AnthropicTextModels: string[] = [
64
67
  // Models in our catalog but not yet publicly available
65
68
  export const AnthropicLimitedAvailabilityModels: string[] = [
66
69
  AnthropicModels.Opus4_8Fast, // fast mode pricing
70
+ AnthropicModels.Opus5Fast, // fast mode pricing (research preview)
67
71
  AnthropicModels.Mythos5, // invitation-only (Project Glasswing)
68
72
  AnthropicModels.MythosPreview, // invitation-only (defensive cybersecurity)
69
73
  AnthropicModels.Opus4_6Fast, // 404 – not publicly available yet
@@ -74,6 +78,11 @@ export const AnthropicLimitedAvailabilityModels: string[] = [
74
78
  const AnthropicAllModels: string[] = [
75
79
  ...AnthropicTextModels,
76
80
  ...AnthropicLimitedAvailabilityModels,
81
+ // Short aliases (resolve to their latest dated version) — kept in the catalog
82
+ // so cost tracking works whether the user passes the alias or the dated id.
83
+ AnthropicModels.Sonnet4_5Short,
84
+ AnthropicModels.Haiku4_5Short,
85
+ AnthropicModels.Opus4_5Short,
77
86
  // Deprecated (retirement 2026-06-15)
78
87
  AnthropicModels.Opus4,
79
88
  AnthropicModels.Sonnet4,
@@ -95,6 +104,9 @@ const AnthropicAllModels: string[] = [
95
104
  export const AnthropicTextPricing: Record<string, ModelPricing> = {
96
105
  [AnthropicModels.Opus4_8]: { input: 5.0, cache_write: 6.25, cache_hit: 0.5, output: 25.0 },
97
106
  [AnthropicModels.Opus4_8Fast]: { input: 10.0, cache_write: 12.5, cache_hit: 1.0, output: 50.0, limitedAvailability: true },
107
+ // Claude Opus 5 (launched 2026-06-09) — recommended for complex agentic coding
108
+ [AnthropicModels.Opus5]: { input: 5.0, cache_write: 6.25, cache_hit: 0.5, output: 25.0 },
109
+ [AnthropicModels.Opus5Fast]: { input: 10.0, cache_write: 12.5, cache_hit: 1.0, output: 50.0, limitedAvailability: true },
98
110
  // Claude 5 generation (launched 2026-06-09)
99
111
  [AnthropicModels.Fable5]: { input: 10.0, cache_write: 12.5, cache_hit: 1.0, output: 50.0 },
100
112
  [AnthropicModels.Sonnet5]: { input: 3.0, cache_write: 3.75, cache_hit: 0.3, output: 15.0 },
@@ -67,6 +67,10 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
67
67
  )
68
68
  .option("--message-id <messageId>", "Knowhow message ID for task tracking")
69
69
  .option("--sync-fs", "Enable filesystem-based synchronization")
70
+ .option(
71
+ "--sync-remote",
72
+ "Push this agent's work to a remote Knowhow task identified by --task-id (creates one if --message-id is given)"
73
+ )
70
74
  .option(
71
75
  "--behavior-id <id>",
72
76
  "Force a specific behavior by its ID (skips trigger matching)"
@@ -79,6 +83,10 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
79
83
  "--task-id <taskId>",
80
84
  "Pre-generated task ID (used with --sync-fs for predictable agent directory path)"
81
85
  )
86
+ .option(
87
+ "--parent-task-id <taskId>",
88
+ "The taskId of the parent agent that spawned this task (for reporting back)"
89
+ )
82
90
  .option("--prompt-file <path>", "Custom prompt template file with {text}")
83
91
  .option("--input <text>", "Task input (fallback to stdin if not provided)")
84
92
  .option(
@@ -139,6 +147,20 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
139
147
  process.exit(1);
140
148
  }
141
149
 
150
+ // If this agent was spawned by a parent agent, inject a note into its
151
+ // prompt so it knows it has a parent it can communicate with — regardless
152
+ // of how it was launched. This is idempotent: startAgentTask no longer
153
+ // adds the parent line itself, so this is the single source of truth.
154
+ if (options.parentTaskId) {
155
+ input =
156
+ input +
157
+ `\n\n---\n` +
158
+ `PARENT AGENT: You were spawned by a parent agent (task "${options.parentTaskId}"). ` +
159
+ `Use the \`replyToParent\` tool to report progress, ask questions, or send your final result back to it. ` +
160
+ `The parent may also send you follow-up messages via your input.txt.\n` +
161
+ `---\n`;
162
+ }
163
+
142
164
  await agentModule.initialize(chatService);
143
165
 
144
166
  // Match a behavior from local disk (.knowhow/behaviors/)