@tyvm/knowhow 0.0.143 → 0.0.145

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 (158) hide show
  1. package/bin/knowhow.js +22 -20
  2. package/package.json +1 -1
  3. package/scripts/migrate-usage-json.js +147 -0
  4. package/src/agents/base/base.ts +65 -10
  5. package/src/agents/tools/patch.ts +84 -2
  6. package/src/agents/tools/textSearch.ts +98 -3
  7. package/src/chat/modules/AgentModule.ts +129 -6
  8. package/src/chat/modules/SessionsModule.ts +6 -0
  9. package/src/chat/types.ts +14 -0
  10. package/src/chat.ts +11 -0
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +56 -1
  13. package/src/clients/contextLimits.ts +40 -0
  14. package/src/clients/gemini.ts +59 -5
  15. package/src/clients/http.ts +85 -4
  16. package/src/clients/index.ts +4 -0
  17. package/src/clients/openai.ts +179 -31
  18. package/src/clients/pricing/fireworks.ts +50 -1
  19. package/src/clients/pricing/index.ts +1 -1
  20. package/src/clients/pricing/models.ts +3 -1
  21. package/src/clients/pricing/openai.ts +2 -2
  22. package/src/clients/pricing/qwen.ts +80 -0
  23. package/src/clients/qwen.ts +16 -0
  24. package/src/clients/types.ts +69 -1
  25. package/src/clients/xai.ts +73 -4
  26. package/src/commands/agent.ts +20 -1
  27. package/src/commands/sandbox.ts +139 -114
  28. package/src/index.ts +9 -0
  29. package/src/processors/Base64ImageDetector.ts +46 -7
  30. package/src/processors/GemmaToolProcessor.ts +163 -0
  31. package/src/processors/index.ts +1 -0
  32. package/src/services/AgentSyncFs.ts +37 -0
  33. package/src/services/SessionManager.ts +14 -0
  34. package/src/services/Tools.ts +3 -0
  35. package/src/services/modules/computerUse.ts +216 -0
  36. package/src/services/modules/index.ts +47 -9
  37. package/src/services/modules/types.ts +23 -1
  38. package/src/types.ts +3 -0
  39. package/tests/clients/contextLimits.test.ts +4 -0
  40. package/tests/clients/openai.images.test.ts +105 -0
  41. package/tests/clients/pricing.test.ts +6 -0
  42. package/tests/compressor/usageBase64.json +159 -0
  43. package/tests/compressor/usageBase64.test.ts +175 -0
  44. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  45. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  46. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  47. package/tests/patching/patchFileOutput.test.ts +112 -1
  48. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  49. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  50. package/tests/unit/commands/sandbox.test.ts +350 -0
  51. package/ts_build/package.json +1 -1
  52. package/ts_build/src/agents/base/base.d.ts +8 -1
  53. package/ts_build/src/agents/base/base.js +37 -3
  54. package/ts_build/src/agents/base/base.js.map +1 -1
  55. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  56. package/ts_build/src/agents/tools/patch.js +35 -2
  57. package/ts_build/src/agents/tools/patch.js.map +1 -1
  58. package/ts_build/src/agents/tools/textSearch.js +56 -2
  59. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  60. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  61. package/ts_build/src/chat/modules/AgentModule.js +73 -5
  62. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  63. package/ts_build/src/chat/modules/SessionsModule.js +4 -0
  64. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  65. package/ts_build/src/chat/types.d.ts +11 -0
  66. package/ts_build/src/chat.js +7 -0
  67. package/ts_build/src/chat.js.map +1 -1
  68. package/ts_build/src/cli.d.ts +2 -1
  69. package/ts_build/src/cli.js +1 -0
  70. package/ts_build/src/cli.js.map +1 -1
  71. package/ts_build/src/clients/anthropic.js +28 -1
  72. package/ts_build/src/clients/anthropic.js.map +1 -1
  73. package/ts_build/src/clients/contextLimits.js +25 -0
  74. package/ts_build/src/clients/contextLimits.js.map +1 -1
  75. package/ts_build/src/clients/gemini.js +48 -4
  76. package/ts_build/src/clients/gemini.js.map +1 -1
  77. package/ts_build/src/clients/http.d.ts +2 -0
  78. package/ts_build/src/clients/http.js +66 -9
  79. package/ts_build/src/clients/http.js.map +1 -1
  80. package/ts_build/src/clients/index.d.ts +1 -0
  81. package/ts_build/src/clients/index.js +4 -0
  82. package/ts_build/src/clients/index.js.map +1 -1
  83. package/ts_build/src/clients/openai.d.ts +6 -1
  84. package/ts_build/src/clients/openai.js +131 -33
  85. package/ts_build/src/clients/openai.js.map +1 -1
  86. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  87. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  88. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  89. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  90. package/ts_build/src/clients/pricing/index.js +6 -2
  91. package/ts_build/src/clients/pricing/index.js.map +1 -1
  92. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  93. package/ts_build/src/clients/pricing/models.js +4 -1
  94. package/ts_build/src/clients/pricing/models.js.map +1 -1
  95. package/ts_build/src/clients/pricing/openai.js +2 -2
  96. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  97. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  98. package/ts_build/src/clients/pricing/qwen.js +30 -0
  99. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  100. package/ts_build/src/clients/qwen.d.ts +4 -0
  101. package/ts_build/src/clients/qwen.js +15 -0
  102. package/ts_build/src/clients/qwen.js.map +1 -0
  103. package/ts_build/src/clients/types.d.ts +12 -0
  104. package/ts_build/src/clients/xai.d.ts +1 -0
  105. package/ts_build/src/clients/xai.js +56 -9
  106. package/ts_build/src/clients/xai.js.map +1 -1
  107. package/ts_build/src/commands/agent.js +10 -1
  108. package/ts_build/src/commands/agent.js.map +1 -1
  109. package/ts_build/src/commands/sandbox.js +79 -80
  110. package/ts_build/src/commands/sandbox.js.map +1 -1
  111. package/ts_build/src/index.d.ts +2 -0
  112. package/ts_build/src/index.js +1 -0
  113. package/ts_build/src/index.js.map +1 -1
  114. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  115. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  116. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  117. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  118. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  119. package/ts_build/src/processors/index.d.ts +1 -0
  120. package/ts_build/src/processors/index.js +3 -1
  121. package/ts_build/src/processors/index.js.map +1 -1
  122. package/ts_build/src/services/AgentSyncFs.js +17 -0
  123. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  124. package/ts_build/src/services/SessionManager.js +12 -0
  125. package/ts_build/src/services/SessionManager.js.map +1 -1
  126. package/ts_build/src/services/Tools.d.ts +2 -0
  127. package/ts_build/src/services/Tools.js.map +1 -1
  128. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  129. package/ts_build/src/services/modules/computerUse.js +3 -0
  130. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  131. package/ts_build/src/services/modules/index.d.ts +1 -0
  132. package/ts_build/src/services/modules/index.js +23 -6
  133. package/ts_build/src/services/modules/index.js.map +1 -1
  134. package/ts_build/src/services/modules/types.d.ts +4 -0
  135. package/ts_build/src/types.d.ts +3 -0
  136. package/ts_build/src/types.js.map +1 -1
  137. package/ts_build/src/workers/tools/index.d.ts +1 -1
  138. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  139. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  140. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  141. package/ts_build/tests/clients/openai.images.test.js +94 -0
  142. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  143. package/ts_build/tests/clients/pricing.test.js +4 -0
  144. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  145. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  146. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  147. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  148. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  149. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  150. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  151. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  152. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  153. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  154. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  155. package/ts_build/tests/unit/commands/sandbox.test.d.ts +1 -0
  156. package/ts_build/tests/unit/commands/sandbox.test.js +258 -0
  157. package/ts_build/tests/unit/commands/sandbox.test.js.map +1 -0
  158. package/x-home.png +0 -0
@@ -257,11 +257,26 @@ export class Base64ImageProcessor {
257
257
  try {
258
258
  // Try to parse as JSON
259
259
  const parsed = JSON.parse(message.content);
260
-
261
- // Check if it's an image_url object
262
- if (parsed.type === "image_url" && parsed.image_url?.url) {
263
- // Convert the tool message content from JSON string to an array with the image
264
- message.content = [parsed];
260
+
261
+ // Normalize the parsed value into an array of candidate parts so we can
262
+ // handle BOTH shapes a tool might return:
263
+ // 1. a single image_url object: {type:"image_url", image_url:{url}}
264
+ // 2. an array of parts: [{type:"image_url", image_url:{url}}, {type:"text", ...}]
265
+ // (Computer-use screenshot tools and loadWebpage return the array form,
266
+ // which the single-object check above previously missed — leaving the
267
+ // model to see a raw base64 JSON string instead of an actual image.)
268
+ const candidates = Array.isArray(parsed) ? parsed : [parsed];
269
+ const isImagePart = (p: any) =>
270
+ p && p.type === "image_url" && p.image_url?.url;
271
+ const isTextPart = (p: any) =>
272
+ p && p.type === "text" && typeof p.text === "string";
273
+
274
+ // Only rewrite the content if it actually contains at least one image
275
+ // part; otherwise leave the original tool text untouched.
276
+ if (candidates.some(isImagePart)) {
277
+ message.content = candidates.filter(
278
+ (p) => isImagePart(p) || isTextPart(p)
279
+ );
265
280
  }
266
281
  } catch (e) {
267
282
  // Not JSON, check if it's a plain base64 string (only if still a string)
@@ -414,10 +429,34 @@ export class Base64ImageProcessor {
414
429
 
415
430
  // Read the file as base64
416
431
  const imageBuffer = fs.readFileSync(filePath);
432
+
433
+ // Detect actual MIME type from magic bytes (overrides extension-based detection)
434
+ // This handles cases where a file is saved with the wrong extension (e.g. JPEG saved as .png)
435
+ let detectedMimeType = mimeType;
436
+ if (imageBuffer.length >= 4) {
437
+ // JPEG: starts with FF D8 FF
438
+ if (imageBuffer[0] === 0xff && imageBuffer[1] === 0xd8 && imageBuffer[2] === 0xff) {
439
+ detectedMimeType = "image/jpeg";
440
+ }
441
+ // PNG: starts with 89 50 4E 47 0D 0A 1A 0A
442
+ else if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50 && imageBuffer[2] === 0x4e && imageBuffer[3] === 0x47) {
443
+ detectedMimeType = "image/png";
444
+ }
445
+ // GIF: starts with 47 49 46 38 (GIF8)
446
+ else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49 && imageBuffer[2] === 0x46 && imageBuffer[3] === 0x38) {
447
+ detectedMimeType = "image/gif";
448
+ }
449
+ // WebP: starts with 52 49 46 46 (RIFF) ... 57 45 42 50 (WEBP) at offset 8
450
+ else if (imageBuffer.length >= 12 && imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49 && imageBuffer[2] === 0x46 && imageBuffer[3] === 0x46 &&
451
+ imageBuffer[8] === 0x57 && imageBuffer[9] === 0x45 && imageBuffer[10] === 0x42 && imageBuffer[11] === 0x50) {
452
+ detectedMimeType = "image/webp";
453
+ }
454
+ }
455
+
417
456
  const base64Data = imageBuffer.toString("base64");
418
457
 
419
- // Create data URL
420
- const dataUrl = `data:${mimeType};base64,${base64Data}`;
458
+ // Create data URL using the actual detected MIME type
459
+ const dataUrl = `data:${detectedMimeType};base64,${base64Data}`;
421
460
 
422
461
  // Return in a format that indicates this is an image
423
462
  // The Base64ImageDetector will convert this to proper image content
@@ -0,0 +1,163 @@
1
+ import { Message, ToolCall } from "../clients/types";
2
+ import { MessageProcessorFunction } from "../services/MessageProcessor";
3
+
4
+ /**
5
+ * Gemma 4 Tool Call Processor
6
+ *
7
+ * Gemma 4 uses a non-standard tool-call format with its own string-quoting token:
8
+ *
9
+ * <|tool_call>call:toolName{key:<|"|>string value<|"|>,numKey:42,boolKey:true}<tool_call|>
10
+ *
11
+ * Strings are delimited by <|"|> instead of standard JSON quotes.
12
+ * Numbers and booleans are written bare (no quotes).
13
+ * Multiple tool calls may appear in a single assistant message.
14
+ *
15
+ * Reference: https://ai.google.dev/gemma/docs/capabilities/text/function-calling-gemma4
16
+ */
17
+ export class GemmaToolProcessor {
18
+ private static instance: GemmaToolProcessor;
19
+
20
+ static getInstance(): GemmaToolProcessor {
21
+ if (!GemmaToolProcessor.instance) {
22
+ GemmaToolProcessor.instance = new GemmaToolProcessor();
23
+ }
24
+ return GemmaToolProcessor.instance;
25
+ }
26
+
27
+ // ── Detection ─────────────────────────────────────────────────────────────
28
+
29
+ /** Returns true when the content looks like it contains Gemma tool-call tokens. */
30
+ private isGemmaFormat(content: string): boolean {
31
+ return content.includes("<|tool_call>") && content.includes("<tool_call|>");
32
+ }
33
+
34
+ // ── Parsing ───────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Cast a raw argument value string to a JS primitive.
38
+ * Mirrors the Python `cast()` helper from the official docs.
39
+ */
40
+ private castValue(raw: string): string | number | boolean {
41
+ const trimmed = raw.trim();
42
+
43
+ // Boolean
44
+ const lower = trimmed.toLowerCase();
45
+ if (lower === "true") return true;
46
+ if (lower === "false") return false;
47
+
48
+ // Integer
49
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
50
+
51
+ // Float
52
+ if (/^-?\d+\.\d*$/.test(trimmed)) return parseFloat(trimmed);
53
+
54
+ // Strip surrounding single/double quotes that the model might emit
55
+ return trimmed.replace(/^['"]|['"]$/g, "");
56
+ }
57
+
58
+ /**
59
+ * Parse the argument body of a single Gemma tool call.
60
+ *
61
+ * The body looks like:
62
+ * key1:<|"|>string value<|"|>,key2:42,key3:true
63
+ *
64
+ * The regex (from the official docs) handles both quoted strings and bare values:
65
+ * (\w+):(?:<|"|>(.*?)<|"|>|([^,}]*))
66
+ */
67
+ private parseArgs(argsBody: string): Record<string, string | number | boolean> {
68
+ const result: Record<string, string | number | boolean> = {};
69
+
70
+ // Regex from the official Google Gemma 4 function-calling docs
71
+ const argPattern = /(\w+):(?:<\|"\|>(.*?)<\|"\|>|([^,}]*))/gs;
72
+ let m: RegExpExecArray | null;
73
+
74
+ while ((m = argPattern.exec(argsBody)) !== null) {
75
+ const key = m[1];
76
+ // v1: string inside <|"|>…<|"|> v2: bare value
77
+ const rawValue = m[2] !== undefined ? m[2] : m[3] ?? "";
78
+ result[key] = m[2] !== undefined ? rawValue : this.castValue(rawValue);
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Extract all Gemma tool calls from an assistant message string.
86
+ * Returns an array of { toolName, arguments } objects.
87
+ */
88
+ private extractGemmaToolCalls(
89
+ content: string
90
+ ): { toolName: string; arguments: string }[] {
91
+ const results: { toolName: string; arguments: string }[] = [];
92
+
93
+ // Outer pattern: <|tool_call>call:name{args}<tool_call|>
94
+ // Using DOTALL-equivalent workaround: [\s\S] for multi-line args
95
+ const outerPattern = /<\|tool_call>call:(\w+)\{([\s\S]*?)\}<tool_call\|>/g;
96
+ let match: RegExpExecArray | null;
97
+
98
+ while ((match = outerPattern.exec(content)) !== null) {
99
+ const toolName = match[1];
100
+ const argsBody = match[2];
101
+
102
+ const parsed = this.parseArgs(argsBody);
103
+ results.push({
104
+ toolName,
105
+ arguments: JSON.stringify(parsed),
106
+ });
107
+ }
108
+
109
+ return results;
110
+ }
111
+
112
+ // ── Conversion ────────────────────────────────────────────────────────────
113
+
114
+ private generateToolCallId(): string {
115
+ return `gemma_call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
116
+ }
117
+
118
+ private toToolCalls(
119
+ extracted: { toolName: string; arguments: string }[]
120
+ ): ToolCall[] {
121
+ return extracted.map(({ toolName, arguments: args }) => ({
122
+ id: this.generateToolCallId(),
123
+ type: "function" as const,
124
+ function: {
125
+ name: toolName,
126
+ arguments: args,
127
+ },
128
+ }));
129
+ }
130
+
131
+ // ── Message processing ────────────────────────────────────────────────────
132
+
133
+ private processMessage(message: Message): void {
134
+ if (message.role !== "assistant") return;
135
+ if (typeof message.content !== "string") return;
136
+ if (message?.tool_calls?.length) return;
137
+
138
+ if (!this.isGemmaFormat(message.content)) return;
139
+
140
+ const extracted = this.extractGemmaToolCalls(message.content);
141
+ if (extracted.length === 0) return;
142
+
143
+ const toolCalls = this.toToolCalls(extracted);
144
+
145
+ if (message.tool_calls) {
146
+ message.tool_calls.push(...toolCalls);
147
+ } else {
148
+ message.tool_calls = toolCalls;
149
+ }
150
+ }
151
+
152
+ // ── Public API ────────────────────────────────────────────────────────────
153
+
154
+ createProcessor(): MessageProcessorFunction {
155
+ return (_originalMessages: Message[], modifiedMessages: Message[]): void => {
156
+ for (const message of modifiedMessages) {
157
+ this.processMessage(message);
158
+ }
159
+ };
160
+ }
161
+ }
162
+
163
+ export const globalGemmaToolProcessor = new GemmaToolProcessor();
@@ -10,4 +10,5 @@ export {
10
10
  export { ToolResponseCache } from "./ToolResponseCache";
11
11
  export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
12
12
  export { HarmonyToolProcessor } from "./HarmonyToolProcessor";
13
+ export { GemmaToolProcessor } from "./GemmaToolProcessor";
13
14
  export { MinimalToolsMessageProcessor } from "./MinimalToolsMessageProcessor";
@@ -133,6 +133,11 @@ export class AgentSyncFs {
133
133
  * that call) to usage.json. This is intended for debugging cache-hit
134
134
  * issues, where comparing the message chains and usage values between
135
135
  * consecutive calls can reveal when a cache prefix was invalidated.
136
+ *
137
+ * To prevent exponential file growth, if the new entry's message thread
138
+ * fully contains the previous entry's thread as a prefix, the previous
139
+ * thread is replaced with { PREV_CACHE_HIT: true } so only the new
140
+ * messages (the delta) are stored alongside the new usage data.
136
141
  */
137
142
  private async appendUsageEntry(entry: any): Promise<void> {
138
143
  if (!this.taskPath) return;
@@ -148,6 +153,27 @@ export class AgentSyncFs {
148
153
  // File doesn't exist or is invalid, start fresh
149
154
  }
150
155
 
156
+ // If the new entry's messages are a superset of the last entry's
157
+ // messages (i.e. the last thread is a prefix of the new thread),
158
+ // collapse the last entry's messages to { PREV_CACHE_HIT: true }
159
+ // and store only the new delta — preventing exponential growth.
160
+ if (entries.length > 0) {
161
+ const lastEntry = entries[entries.length - 1];
162
+ const prevMessages: any[] = lastEntry?.messages;
163
+ const newMessages: any[] = entry?.messages;
164
+ if (
165
+ Array.isArray(prevMessages) &&
166
+ Array.isArray(newMessages) &&
167
+ prevMessages.length > 0 &&
168
+ newMessages.length >= prevMessages.length &&
169
+ JSON.stringify(newMessages.slice(0, prevMessages.length)) ===
170
+ JSON.stringify(prevMessages)
171
+ ) {
172
+ // Replace the previous full thread with a compact sentinel
173
+ lastEntry.messages = [{ PREV_CACHE_HIT: true }];
174
+ }
175
+ }
176
+
151
177
  entries.push(entry);
152
178
  await fs.writeFile(usagePath, JSON.stringify(entries, null, 2), "utf8");
153
179
  } catch (error) {
@@ -180,6 +206,17 @@ export class AgentSyncFs {
180
206
  metadata.totalCostUsd = agent.getTotalCostUsd();
181
207
  metadata.tokenUsage = agent.getTokenUsage();
182
208
  metadata.agentName = agent.name;
209
+ // Record the model/provider actually in use at this sync so task
210
+ // history reflects the real model (including mid-run fallbacks), not
211
+ // just the named agent.
212
+ metadata.model = agent.getModel();
213
+ metadata.provider = agent.getProvider();
214
+ // Persist reasoning settings so a resume can restore the exact
215
+ // reasoning effort / summarize behavior the run was using. (The
216
+ // encrypted reasoning items + summaries themselves ride along on the
217
+ // thread messages via `_reasoning_details` / `reasoning_summary`.)
218
+ metadata.reasoningEffort = agent.getReasoningEffort?.();
219
+ metadata.summarizeReasoning = agent.getSummarizeReasoning?.();
183
220
  metadata.inProgress = inProgress;
184
221
  metadata.lastUpdate = new Date().toISOString();
185
222
 
@@ -65,6 +65,10 @@ export class SessionManager {
65
65
  threads,
66
66
  currentThread: 0,
67
67
  lastUpdated: Date.now(),
68
+ model: taskInfo.model,
69
+ provider: taskInfo.provider,
70
+ reasoningEffort: taskInfo.reasoningEffort,
71
+ summarizeReasoning: taskInfo.summarizeReasoning,
68
72
  };
69
73
 
70
74
  fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
@@ -101,6 +105,16 @@ export class SessionManager {
101
105
  session.knowhowMessageId = taskInfo.knowhowMessageId;
102
106
  session.knowhowTaskId = taskInfo.knowhowTaskId;
103
107
  session.chatSessionId = taskInfo.chatSessionId;
108
+
109
+ // Keep the persisted run configuration in sync (captures mid-run
110
+ // model/provider fallbacks) so a resume restores the exact setup.
111
+ if (taskInfo.model !== undefined) session.model = taskInfo.model;
112
+ if (taskInfo.provider !== undefined)
113
+ session.provider = taskInfo.provider;
114
+ if (taskInfo.reasoningEffort !== undefined)
115
+ session.reasoningEffort = taskInfo.reasoningEffort;
116
+ if (taskInfo.summarizeReasoning !== undefined)
117
+ session.summarizeReasoning = taskInfo.summarizeReasoning;
104
118
  }
105
119
 
106
120
  fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
@@ -34,6 +34,8 @@ export interface ToolCallContext {
34
34
  caller?: any;
35
35
  /** The task id of the calling agent, if known. */
36
36
  taskId?: string;
37
+ /** The task id of the parent agent that spawned the calling agent, if any. */
38
+ parentTaskId?: string;
37
39
  /** Arbitrary additional per-call context. */
38
40
  [key: string]: any;
39
41
  }
@@ -46,6 +48,7 @@ export interface ToolContext {
46
48
  Plugins?: PluginService;
47
49
  Mcp?: McpService;
48
50
  Behaviors?: BehaviorsService;
51
+ ComputerUse?: import("./modules/computerUse").ComputerUseService;
49
52
  metadata?: { [key: string]: any };
50
53
  }
51
54
 
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Shared, driver-agnostic interface for the computer-use service.
3
+ *
4
+ * The concrete implementation lives in `@tyvm/knowhow-module-computer-use`, but
5
+ * we declare the shape here so that `ToolContext` / `ModuleContext` can carry a
6
+ * `ComputerUse` service without the core knowhow package taking a runtime
7
+ * dependency on the module.
8
+ *
9
+ * An adapter module (e.g. `@tyvm/knowhow-module-computer-use-nutjs`) can
10
+ * register a `ComputerDriver` against the service in its `register` phase; the
11
+ * base module then `init()`s and picks the best available driver.
12
+ */
13
+
14
+ export interface Point {
15
+ x: number;
16
+ y: number;
17
+ }
18
+
19
+ export interface Size {
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface Region extends Point, Size {}
25
+
26
+ export type MouseButton = "left" | "right" | "middle";
27
+
28
+ export interface OverlayPrimitive {
29
+ kind: "rect" | "circle" | "line" | "point";
30
+ x: number;
31
+ y: number;
32
+ width?: number;
33
+ height?: number;
34
+ x2?: number;
35
+ y2?: number;
36
+ color?: string;
37
+ lineWidth?: number;
38
+ }
39
+
40
+ export interface Display {
41
+ id: number;
42
+ bounds: Region;
43
+ scaleFactor: number;
44
+ primary: boolean;
45
+ }
46
+
47
+ export interface ScreenshotOptions {
48
+ region?: Region;
49
+ displayId?: number;
50
+ format?: "png" | "jpeg";
51
+ scale?: number;
52
+ /** Native capture downscale. Unlike output encoding scale, drivers may apply
53
+ * this before raw pixels cross the process boundary. */
54
+ captureScale?: number;
55
+ }
56
+
57
+ export interface DriverCapabilities {
58
+ input: boolean;
59
+ capture: boolean;
60
+ windows: boolean;
61
+ reason?: string;
62
+ }
63
+
64
+ export interface AccessibilityElement {
65
+ id: string;
66
+ role: string;
67
+ subrole?: string;
68
+ title?: string;
69
+ description?: string;
70
+ value?: string;
71
+ enabled?: boolean;
72
+ focused?: boolean;
73
+ bounds?: Region;
74
+ actions: string[];
75
+ childCount: number;
76
+ }
77
+
78
+ export interface AccessibilityOptions {
79
+ maxDepth?: number;
80
+ maxElements?: number;
81
+ interactiveOnly?: boolean;
82
+ }
83
+
84
+ /**
85
+ * The engine seam. Concrete drivers (Rust core, CLI adapter, nut.js demo)
86
+ * implement this. Registered with the service via `registerDriver`.
87
+ */
88
+ export interface ComputerDriver {
89
+ readonly name: string;
90
+ /**
91
+ * Lower numbers win when multiple drivers are registered. The default Rust
92
+ * core registers at 100; a fallback CLI adapter at 500; an experimental demo
93
+ * adapter can register at e.g. 10 to force selection.
94
+ */
95
+ readonly priority?: number;
96
+
97
+ capabilities(): Promise<DriverCapabilities>;
98
+
99
+ // screen
100
+ getDisplays(): Promise<Display[]>;
101
+ screenSize(): Promise<Size>;
102
+ screenshot(opts?: ScreenshotOptions): Promise<Buffer>;
103
+ pixelColor(p: Point): Promise<string>;
104
+
105
+ // mouse
106
+ mousePosition(): Promise<Point>;
107
+ moveMouse(p: Point, opts?: { duration?: number }): Promise<void>;
108
+ click(button?: MouseButton, opts?: { double?: boolean }): Promise<void>;
109
+ mouseDown(button?: MouseButton): Promise<void>;
110
+ mouseUp(button?: MouseButton): Promise<void>;
111
+ drag(
112
+ from: Point,
113
+ to: Point,
114
+ opts?: { button?: MouseButton; duration?: number }
115
+ ): Promise<void>;
116
+ scroll(dx: number, dy: number): Promise<void>;
117
+
118
+ // keyboard
119
+ typeText(text: string, opts?: { delay?: number }): Promise<void>;
120
+ pressKey(key: string): Promise<void>;
121
+ hotkey(...keys: string[]): Promise<void>;
122
+ keyDown(key: string): Promise<void>;
123
+ keyUp(key: string): Promise<void>;
124
+
125
+ // windows (optional / capability-gated)
126
+ getActiveWindow?(): Promise<{ title: string; app?: string; bounds?: Region } | null>;
127
+ listWindows?(): Promise<Array<{ title: string; app?: string; bounds?: Region }>>;
128
+ focusWindow?(match: string): Promise<boolean>;
129
+
130
+ accessibilityTrusted?(): Promise<boolean>;
131
+ accessibilityElements?(options?: AccessibilityOptions): Promise<AccessibilityElement[]>;
132
+ setAccessibilityValue?(id: string, value: string): Promise<void>;
133
+ performAccessibilityAction?(id: string, action: string): Promise<void>;
134
+
135
+ // click-through visual debugging (optional / platform capability-gated)
136
+ showOverlay?(primitives: OverlayPrimitive[]): Promise<void>;
137
+ clearOverlay?(): Promise<void>;
138
+
139
+ dispose?(): Promise<void>;
140
+ }
141
+
142
+ /**
143
+ * The registered service (`ComputerUse`) surfaced to tools/CLI/MCP. It owns
144
+ * driver selection and provides the normalized, platform-agnostic verbs.
145
+ *
146
+ * We keep this as an interface (not a class) in the core package so the module
147
+ * owns the concrete implementation; anything reading `context.ComputerUse`
148
+ * codes against this contract.
149
+ */
150
+ export interface ComputerUseService {
151
+ /** Register a driver (called by the base module and any adapter modules). */
152
+ registerDriver(driver: ComputerDriver): void;
153
+ /** List the names of all registered drivers. */
154
+ listDrivers(): string[];
155
+ /** Resolve/select the active driver (highest priority + capable). */
156
+ getDriver(): Promise<ComputerDriver>;
157
+
158
+ capabilities(): Promise<DriverCapabilities>;
159
+ getDisplays(): Promise<Display[]>;
160
+ screenSize(): Promise<Size>;
161
+ /** Returns encoded image bytes (PNG/JPEG) honoring service defaults. */
162
+ screenshot(opts?: ScreenshotOptions): Promise<Buffer>;
163
+ /** Convenience: base64 data (no data: prefix) for multimodal messages. */
164
+ screenshotBase64(opts?: ScreenshotOptions): Promise<string>;
165
+ pixelColor(p: Point): Promise<string>;
166
+
167
+ mousePosition(): Promise<Point>;
168
+ moveMouse(p: Point, opts?: { duration?: number }): Promise<void>;
169
+ click(button?: MouseButton, opts?: { double?: boolean }): Promise<void>;
170
+ drag(
171
+ from: Point,
172
+ to: Point,
173
+ opts?: { button?: MouseButton; duration?: number }
174
+ ): Promise<void>;
175
+ scroll(dx: number, dy: number): Promise<void>;
176
+ /** Smooth stepped scrolling (repeat a small delta with a pause between). */
177
+ scrollBy?(
178
+ dx: number,
179
+ dy: number,
180
+ opts?: { repeat?: number; intervalMs?: number }
181
+ ): Promise<void>;
182
+
183
+ typeText(text: string, opts?: { delay?: number }): Promise<void>;
184
+ pressKey(key: string): Promise<void>;
185
+ hotkey(...keys: string[]): Promise<void>;
186
+
187
+ /** Screenshot with an optional labeled grid and/or crosshair overlay. */
188
+ screenshotAnnotated?(
189
+ opts?: ScreenshotOptions & {
190
+ grid?: boolean;
191
+ gridStep?: number;
192
+ crosshair?: Point;
193
+ }
194
+ ): Promise<Buffer>;
195
+
196
+ getActiveWindow(): Promise<{
197
+ title: string;
198
+ app?: string;
199
+ bounds?: Region;
200
+ } | null>;
201
+ listWindows(): Promise<
202
+ Array<{ title: string; app?: string; bounds?: Region }>
203
+ >;
204
+ focusWindow(match: string): Promise<boolean>;
205
+
206
+ accessibilityTrusted(): Promise<boolean>;
207
+ accessibilityElements(options?: AccessibilityOptions): Promise<AccessibilityElement[]>;
208
+ selectAccessibilityOption(id: string, option: string): Promise<void>;
209
+ setAccessibilityValue(id: string, value: string): Promise<void>;
210
+ performAccessibilityAction(id: string, action: string): Promise<void>;
211
+
212
+ showOverlay(primitives: OverlayPrimitive[]): Promise<void>;
213
+ clearOverlay(): Promise<void>;
214
+
215
+ dispose(): Promise<void>;
216
+ }
@@ -75,6 +75,21 @@ export class ModulesService {
75
75
  // modules resolve against cwd/.knowhow/node_modules first. See localResolvePaths()
76
76
  // and globalResolvePaths() above.
77
77
 
78
+ // Two-phase load:
79
+ // Phase 1 (register): resolve + require EVERY module and run its optional
80
+ // `register()`, which may (a) register CLI subcommands on Program and
81
+ // (b) inject shared services into `context` (e.g. `ComputerUse`) so that
82
+ // sibling/adapter modules loaded later can consume them.
83
+ // Phase 2 (init): run every loaded module's `init()`, now that all
84
+ // services registered during phase 1 are present. This lets, e.g., a
85
+ // `-nutjs` adapter module register a driver against the base
86
+ // computer-use service during phase 1, and the base module resolve the
87
+ // best driver during phase 2.
88
+ const loadedModules: {
89
+ modulePath: string;
90
+ module: KnowhowModule;
91
+ }[] = [];
92
+
78
93
  for (const modulePath of allModulePaths) {
79
94
  // Build an ordered list of candidate resolutions for this module.
80
95
  // Relative paths resolve to a single candidate (relative to cwd), while
@@ -122,15 +137,16 @@ export class ModulesService {
122
137
  "ModulesService",
123
138
  `šŸ”Œ Loading module: ${modulePath} (resolved: ${resolvedPath})`
124
139
  );
125
- await importedModule.init({
126
- config,
127
- cwd: process.cwd(),
128
- context: context as ModuleContext,
129
- });
130
- context.Events?.log(
131
- "ModulesService",
132
- `āœ… Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
133
- );
140
+ // Phase 1: register (optional). Injects services into `context` and
141
+ // registers CLI commands. Must be idempotent (may run in the early
142
+ // Program-only CLI phase AND the full-services phase).
143
+ if (typeof importedModule.register === "function") {
144
+ await importedModule.register({
145
+ config,
146
+ cwd: process.cwd(),
147
+ context: context as ModuleContext,
148
+ });
149
+ }
134
150
  loaded = true;
135
151
  break;
136
152
  } catch (err: any) {
@@ -152,6 +168,28 @@ export class ModulesService {
152
168
  );
153
169
  continue;
154
170
  }
171
+ loadedModules.push({ modulePath, module: importedModule });
172
+ }
173
+
174
+ // Phase 2: init every successfully-loaded module now that all `register`
175
+ // phases have run and injected their services into `context`.
176
+ for (const { modulePath, module: importedModule } of loadedModules) {
177
+ try {
178
+ await importedModule.init({
179
+ config,
180
+ cwd: process.cwd(),
181
+ context: context as ModuleContext,
182
+ });
183
+ context.Events?.log(
184
+ "ModulesService",
185
+ `āœ… Module initialized: ${modulePath} (tools: ${importedModule.tools.length}, agents: ${importedModule.agents.length}, plugins: ${importedModule.plugins.length}, clients: ${importedModule.clients.length})`
186
+ );
187
+ } catch (err: any) {
188
+ process.stderr.write(
189
+ `\nāš ļø Failed to init module "${modulePath}": ${err?.message || err}\n\n`
190
+ );
191
+ continue;
192
+ }
155
193
  // Only register tools/agents/plugins/clients if the relevant services
156
194
  // are available in context (they may not be during early CLI command registration)
157
195
  if (context.Agents) {
@@ -14,6 +14,7 @@ import { TunnelHandler } from "@tyvm/knowhow-tunnel";
14
14
  import { EventService } from "../EventService";
15
15
  import { ConversionService } from "../conversion/ConversionService";
16
16
  import { BehaviorsService } from "../BehaviorsService";
17
+ import { ComputerUseService } from "./computerUse";
17
18
 
18
19
  /*
19
20
  *
@@ -62,10 +63,31 @@ export interface ModuleContext {
62
63
  Conversion?: ConversionService;
63
64
  Tunnel?: TunnelHandler;
64
65
  Program?: Command;
65
- Behaviors?: BehaviorsService
66
+ Behaviors?: BehaviorsService;
67
+ ComputerUse?: ComputerUseService;
68
+ [key: string]: any;
66
69
  }
67
70
 
68
71
  export interface KnowhowModule {
72
+ /**
73
+ * Phase 1 (optional): called first, potentially with ONLY `Program` in
74
+ * context (e.g. during early CLI command registration). Use this to:
75
+ * - register CLI subcommands on `context.Program`
76
+ * - inject services into the shared context (e.g.
77
+ * `context.Tools?.addContext("ComputerUse", svc)`) so that OTHER modules
78
+ * (or a sibling adapter module) can consume them during their own init.
79
+ *
80
+ * `register` must be side-effect-light and MUST NOT assume the full service
81
+ * graph is present — only `Program` is guaranteed during the early phase.
82
+ * It may be called more than once (early CLI phase + full-services phase);
83
+ * implementations should be idempotent.
84
+ */
85
+ register?: (params: InitParams) => Promise<void>;
86
+ /**
87
+ * Phase 2: called with the full service graph available. Use this to
88
+ * actually connect/start things (open drivers, spin up watchers, read
89
+ * services registered by other modules' `register` phase, etc.).
90
+ */
69
91
  init: (params: InitParams) => Promise<void>;
70
92
  commands: ModuleChatCommand[];
71
93
  tools: ModuleTool[];