@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.
- package/bin/knowhow.js +22 -20
- package/package.json +1 -1
- package/scripts/migrate-usage-json.js +147 -0
- package/src/agents/base/base.ts +65 -10
- package/src/agents/tools/patch.ts +84 -2
- package/src/agents/tools/textSearch.ts +98 -3
- package/src/chat/modules/AgentModule.ts +129 -6
- package/src/chat/modules/SessionsModule.ts +6 -0
- package/src/chat/types.ts +14 -0
- package/src/chat.ts +11 -0
- package/src/cli.ts +2 -0
- package/src/clients/anthropic.ts +56 -1
- package/src/clients/contextLimits.ts +40 -0
- package/src/clients/gemini.ts +59 -5
- package/src/clients/http.ts +85 -4
- package/src/clients/index.ts +4 -0
- package/src/clients/openai.ts +179 -31
- package/src/clients/pricing/fireworks.ts +50 -1
- package/src/clients/pricing/index.ts +1 -1
- package/src/clients/pricing/models.ts +3 -1
- package/src/clients/pricing/openai.ts +2 -2
- package/src/clients/pricing/qwen.ts +80 -0
- package/src/clients/qwen.ts +16 -0
- package/src/clients/types.ts +69 -1
- package/src/clients/xai.ts +73 -4
- package/src/commands/agent.ts +20 -1
- package/src/commands/sandbox.ts +139 -114
- package/src/index.ts +9 -0
- package/src/processors/Base64ImageDetector.ts +46 -7
- package/src/processors/GemmaToolProcessor.ts +163 -0
- package/src/processors/index.ts +1 -0
- package/src/services/AgentSyncFs.ts +37 -0
- package/src/services/SessionManager.ts +14 -0
- package/src/services/Tools.ts +3 -0
- package/src/services/modules/computerUse.ts +216 -0
- package/src/services/modules/index.ts +47 -9
- package/src/services/modules/types.ts +23 -1
- package/src/types.ts +3 -0
- package/tests/clients/contextLimits.test.ts +4 -0
- package/tests/clients/openai.images.test.ts +105 -0
- package/tests/clients/pricing.test.ts +6 -0
- package/tests/compressor/usageBase64.json +159 -0
- package/tests/compressor/usageBase64.test.ts +175 -0
- package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
- package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
- package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
- package/tests/patching/patchFileOutput.test.ts +112 -1
- package/tests/patching/transcriptMangleRegression.test.ts +125 -0
- package/tests/processors/Base64ImageDetector.test.ts +36 -1
- package/tests/unit/commands/sandbox.test.ts +350 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +8 -1
- package/ts_build/src/agents/base/base.js +37 -3
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/patch.d.ts +1 -0
- package/ts_build/src/agents/tools/patch.js +35 -2
- package/ts_build/src/agents/tools/patch.js.map +1 -1
- package/ts_build/src/agents/tools/textSearch.js +56 -2
- package/ts_build/src/agents/tools/textSearch.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
- package/ts_build/src/chat/modules/AgentModule.js +73 -5
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +4 -0
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +11 -0
- package/ts_build/src/chat.js +7 -0
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/cli.d.ts +2 -1
- package/ts_build/src/cli.js +1 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +28 -1
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +25 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/gemini.js +48 -4
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +2 -0
- package/ts_build/src/clients/http.js +66 -9
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +1 -0
- package/ts_build/src/clients/index.js +4 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +6 -1
- package/ts_build/src/clients/openai.js +131 -33
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
- package/ts_build/src/clients/pricing/fireworks.js +33 -1
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/clients/pricing/index.d.ts +1 -1
- package/ts_build/src/clients/pricing/index.js +6 -2
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/models.d.ts +2 -1
- package/ts_build/src/clients/pricing/models.js +4 -1
- package/ts_build/src/clients/pricing/models.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +2 -2
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
- package/ts_build/src/clients/pricing/qwen.js +30 -0
- package/ts_build/src/clients/pricing/qwen.js.map +1 -0
- package/ts_build/src/clients/qwen.d.ts +4 -0
- package/ts_build/src/clients/qwen.js +15 -0
- package/ts_build/src/clients/qwen.js.map +1 -0
- package/ts_build/src/clients/types.d.ts +12 -0
- package/ts_build/src/clients/xai.d.ts +1 -0
- package/ts_build/src/clients/xai.js +56 -9
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/commands/agent.js +10 -1
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/sandbox.js +79 -80
- package/ts_build/src/commands/sandbox.js.map +1 -1
- package/ts_build/src/index.d.ts +2 -0
- package/ts_build/src/index.js +1 -0
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.js +22 -3
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
- package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
- package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
- package/ts_build/src/processors/index.d.ts +1 -0
- package/ts_build/src/processors/index.js +3 -1
- package/ts_build/src/processors/index.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.js +17 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/SessionManager.js +12 -0
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +2 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/modules/computerUse.d.ts +163 -0
- package/ts_build/src/services/modules/computerUse.js +3 -0
- package/ts_build/src/services/modules/computerUse.js.map +1 -0
- package/ts_build/src/services/modules/index.d.ts +1 -0
- package/ts_build/src/services/modules/index.js +23 -6
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +4 -0
- package/ts_build/src/types.d.ts +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/workers/tools/index.d.ts +1 -1
- package/ts_build/tests/clients/contextLimits.test.js +2 -0
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
- package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
- package/ts_build/tests/clients/openai.images.test.js +94 -0
- package/ts_build/tests/clients/openai.images.test.js.map +1 -0
- package/ts_build/tests/clients/pricing.test.js +4 -0
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
- package/ts_build/tests/compressor/usageBase64.test.js +109 -0
- package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
- package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
- package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
- package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
- package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/unit/commands/sandbox.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/sandbox.test.js +258 -0
- package/ts_build/tests/unit/commands/sandbox.test.js.map +1 -0
- 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
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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:${
|
|
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();
|
package/src/processors/index.ts
CHANGED
|
@@ -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));
|
package/src/services/Tools.ts
CHANGED
|
@@ -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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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[];
|