@tyvm/knowhow 0.0.128 → 0.0.130
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/package.json +5 -4
- package/scripts/cache-miss-debug.ts +365 -0
- package/src/agents/base/base.ts +159 -59
- package/src/agents/tools/execCommand.ts +26 -6
- package/src/agents/tools/index.ts +1 -0
- package/src/agents/tools/list.ts +2 -0
- package/src/agents/tools/minimal/callTool.ts +45 -0
- package/src/agents/tools/minimal/definitions.ts +50 -0
- package/src/agents/tools/minimal/index.ts +3 -0
- package/src/agents/tools/minimal/inspectTools.ts +27 -0
- package/src/agents/tools/skills.ts +79 -0
- package/src/chat/modules/AgentModule.ts +111 -9
- package/src/cli.ts +4 -0
- package/src/clients/anthropic.ts +59 -1
- package/src/clients/contextLimits.ts +9 -0
- package/src/clients/index.ts +6 -0
- package/src/clients/meta.ts +18 -0
- package/src/clients/openai.ts +28 -5
- package/src/clients/pricing/anthropic.ts +13 -0
- package/src/clients/pricing/fireworks.ts +18 -12
- package/src/clients/pricing/index.ts +1 -0
- package/src/clients/pricing/meta.ts +12 -0
- package/src/clients/pricing/openai.ts +16 -0
- package/src/clients/pricing/xai.ts +3 -0
- package/src/clients/types.ts +27 -4
- package/src/clients/xai.ts +2 -2
- package/src/commands/agent.ts +19 -0
- package/src/commands/behaviors.ts +47 -0
- package/src/commands/services.ts +7 -3
- package/src/commands/skills.ts +71 -0
- package/src/index.ts +2 -0
- package/src/plugins/PluginBase.ts +0 -2
- package/src/plugins/SkillsPlugin.ts +11 -124
- package/src/plugins/language.ts +0 -1
- package/src/plugins/plugins.ts +24 -13
- package/src/plugins/types.ts +8 -2
- package/src/processors/MinimalToolsMessageProcessor.ts +113 -0
- package/src/processors/TokenCompressor.ts +21 -4
- package/src/processors/index.ts +7 -1
- package/src/services/AgentSyncFs.ts +49 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/BehaviorsService.ts +432 -0
- package/src/services/KnowhowClient.ts +62 -1
- package/src/services/LazyToolsService.ts +12 -0
- package/src/services/MinimalToolsService.ts +167 -0
- package/src/services/SkillsService.ts +622 -0
- package/src/services/Tools.ts +11 -0
- package/src/services/index.ts +9 -2
- package/src/services/modules/types.ts +2 -0
- package/src/worker.ts +7 -0
- package/tests/processors/TokenCompressor.test.ts +45 -0
- package/tests/unit/services/MinimalToolsService.test.ts +424 -0
- package/ts_build/package.json +5 -4
- package/ts_build/src/agents/base/base.d.ts +13 -2
- package/ts_build/src/agents/base/base.js +91 -35
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +18 -5
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +1 -0
- package/ts_build/src/agents/tools/index.js +1 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/minimal/callTool.d.ts +5 -0
- package/ts_build/src/agents/tools/minimal/callTool.js +28 -0
- package/ts_build/src/agents/tools/minimal/callTool.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/definitions.d.ts +4 -0
- package/ts_build/src/agents/tools/minimal/definitions.js +46 -0
- package/ts_build/src/agents/tools/minimal/definitions.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/index.d.ts +3 -0
- package/ts_build/src/agents/tools/minimal/index.js +20 -0
- package/ts_build/src/agents/tools/minimal/index.js.map +1 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.d.ts +2 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js +15 -0
- package/ts_build/src/agents/tools/minimal/inspectTools.js.map +1 -0
- package/ts_build/src/agents/tools/skills.d.ts +33 -0
- package/ts_build/src/agents/tools/skills.js +62 -0
- package/ts_build/src/agents/tools/skills.js.map +1 -0
- package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
- package/ts_build/src/chat/modules/AgentModule.js +77 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.d.ts +1 -1
- package/ts_build/src/cli.js +4 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +2 -0
- package/ts_build/src/clients/anthropic.js +35 -0
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +8 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +1 -0
- package/ts_build/src/clients/index.js +6 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/meta.d.ts +4 -0
- package/ts_build/src/clients/meta.js +15 -0
- package/ts_build/src/clients/meta.js.map +1 -0
- package/ts_build/src/clients/openai.d.ts +1 -0
- package/ts_build/src/clients/openai.js +25 -5
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +4 -0
- package/ts_build/src/clients/pricing/anthropic.js +12 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +2 -5
- package/ts_build/src/clients/pricing/fireworks.js +13 -11
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/clients/pricing/index.d.ts +1 -0
- package/ts_build/src/clients/pricing/index.js +5 -2
- package/ts_build/src/clients/pricing/index.js.map +1 -1
- package/ts_build/src/clients/pricing/meta.d.ts +6 -0
- package/ts_build/src/clients/pricing/meta.js +8 -0
- package/ts_build/src/clients/pricing/meta.js.map +1 -0
- package/ts_build/src/clients/pricing/openai.d.ts +3 -0
- package/ts_build/src/clients/pricing/openai.js +15 -0
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/xai.d.ts +1 -0
- package/ts_build/src/clients/pricing/xai.js +3 -0
- package/ts_build/src/clients/pricing/xai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +6 -1
- package/ts_build/src/clients/xai.js +1 -1
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/commands/agent.js +16 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/behaviors.d.ts +2 -0
- package/ts_build/src/commands/behaviors.js +43 -0
- package/ts_build/src/commands/behaviors.js.map +1 -0
- package/ts_build/src/commands/services.js +4 -2
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/commands/skills.d.ts +2 -0
- package/ts_build/src/commands/skills.js +61 -0
- package/ts_build/src/commands/skills.js.map +1 -0
- package/ts_build/src/index.d.ts +2 -0
- package/ts_build/src/index.js +5 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/plugins/PluginBase.d.ts +0 -2
- package/ts_build/src/plugins/PluginBase.js.map +1 -1
- package/ts_build/src/plugins/SkillsPlugin.d.ts +1 -6
- package/ts_build/src/plugins/SkillsPlugin.js +6 -135
- package/ts_build/src/plugins/SkillsPlugin.js.map +1 -1
- package/ts_build/src/plugins/language.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +4 -1
- package/ts_build/src/plugins/plugins.js +17 -11
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/types.d.ts +3 -2
- package/ts_build/src/processors/MinimalToolsMessageProcessor.d.ts +7 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js +61 -0
- package/ts_build/src/processors/MinimalToolsMessageProcessor.js.map +1 -0
- package/ts_build/src/processors/TokenCompressor.js +5 -2
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/processors/index.d.ts +2 -1
- 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.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +30 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +1 -1
- package/ts_build/src/services/BehaviorsService.d.ts +46 -0
- package/ts_build/src/services/BehaviorsService.js +358 -0
- package/ts_build/src/services/BehaviorsService.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +9 -1
- package/ts_build/src/services/KnowhowClient.js +20 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +14 -0
- package/ts_build/src/services/MinimalToolsService.js +80 -0
- package/ts_build/src/services/MinimalToolsService.js.map +1 -0
- package/ts_build/src/services/SkillsService.d.ts +53 -0
- package/ts_build/src/services/SkillsService.js +478 -0
- package/ts_build/src/services/SkillsService.js.map +1 -0
- 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/index.d.ts +4 -0
- package/ts_build/src/services/index.js +9 -2
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +4 -2
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/src/types.d.ts +8 -0
- package/ts_build/src/worker.js +5 -0
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +27 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/services/MinimalToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/MinimalToolsService.test.js +320 -0
- package/ts_build/tests/unit/services/MinimalToolsService.test.js.map +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Message, ToolCall } from "../clients/types";
|
|
2
|
+
import { MessageProcessorFunction } from "../services/MessageProcessor";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MinimalToolsMessageProcessor
|
|
6
|
+
*
|
|
7
|
+
* Works with MinimalToolsService to unwrap `callTool(name, args)` invocations
|
|
8
|
+
* in the message history, rewriting them as if the real tool had been called
|
|
9
|
+
* directly.
|
|
10
|
+
*
|
|
11
|
+
* This is registered on the `pre_tools` lifecycle so it runs right before tool
|
|
12
|
+
* calls are dispatched. It rewrites the last assistant message's tool_calls so
|
|
13
|
+
* that:
|
|
14
|
+
* callTool({ name: "readFile", args: { filePath: "..." } })
|
|
15
|
+
* becomes:
|
|
16
|
+
* readFile({ filePath: "..." })
|
|
17
|
+
*
|
|
18
|
+
* This means:
|
|
19
|
+
* 1. The `tools:` array sent to the AI never needs to change (cache stable).
|
|
20
|
+
* 2. The agent terminates correctly when callTool("finalAnswer", ...) is called,
|
|
21
|
+
* because after transformation the required tool name matches.
|
|
22
|
+
* 3. The conversation history looks clean — the model sees native tool calls in
|
|
23
|
+
* subsequent turns rather than nested callTool wrappers.
|
|
24
|
+
*/
|
|
25
|
+
export class MinimalToolsMessageProcessor {
|
|
26
|
+
/**
|
|
27
|
+
* Unwraps a single callTool ToolCall into the real underlying ToolCall.
|
|
28
|
+
* Returns null if this is not a callTool invocation.
|
|
29
|
+
*/
|
|
30
|
+
private unwrapCallTool(toolCall: ToolCall): ToolCall | null {
|
|
31
|
+
if (toolCall.function.name !== "callTool") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let parsed: { name?: string; args?: Record<string, any> };
|
|
36
|
+
try {
|
|
37
|
+
parsed =
|
|
38
|
+
typeof toolCall.function.arguments === "string"
|
|
39
|
+
? JSON.parse(toolCall.function.arguments)
|
|
40
|
+
: toolCall.function.arguments;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { name, args } = parsed;
|
|
46
|
+
if (!name || typeof name !== "string") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: toolCall.id,
|
|
52
|
+
type: "function",
|
|
53
|
+
function: {
|
|
54
|
+
name,
|
|
55
|
+
arguments: JSON.stringify(args ?? {}),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Processes the modified messages array in-place:
|
|
62
|
+
* - Finds assistant messages whose tool_calls contain callTool invocations.
|
|
63
|
+
* - Replaces those callTool entries with the real underlying tool calls.
|
|
64
|
+
*
|
|
65
|
+
* Only the most-recent assistant message (the one just added by the AI) is
|
|
66
|
+
* rewritten — older messages are left untouched so caching is not affected.
|
|
67
|
+
*/
|
|
68
|
+
private processMessages(
|
|
69
|
+
_originalMessages: Message[],
|
|
70
|
+
modifiedMessages: Message[]
|
|
71
|
+
): void {
|
|
72
|
+
// Walk backwards to find the last assistant message with tool_calls
|
|
73
|
+
for (let i = modifiedMessages.length - 1; i >= 0; i--) {
|
|
74
|
+
const msg = modifiedMessages[i];
|
|
75
|
+
if (msg.role !== "assistant" || !msg.tool_calls?.length) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let didUnwrap = false;
|
|
80
|
+
const newToolCalls: ToolCall[] = msg.tool_calls.map((tc) => {
|
|
81
|
+
const unwrapped = this.unwrapCallTool(tc);
|
|
82
|
+
if (unwrapped) {
|
|
83
|
+
didUnwrap = true;
|
|
84
|
+
return unwrapped;
|
|
85
|
+
}
|
|
86
|
+
return tc;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (didUnwrap) {
|
|
90
|
+
msg.tool_calls = newToolCalls;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only process the last assistant message
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns a MessageProcessorFunction suitable for registering on the
|
|
100
|
+
* `pre_tools` lifecycle of a MessageProcessor.
|
|
101
|
+
*/
|
|
102
|
+
createProcessor(): MessageProcessorFunction {
|
|
103
|
+
return (
|
|
104
|
+
originalMessages: Message[],
|
|
105
|
+
modifiedMessages: Message[]
|
|
106
|
+
): void => {
|
|
107
|
+
this.processMessages(originalMessages, modifiedMessages);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const globalMinimalToolsMessageProcessor =
|
|
113
|
+
new MinimalToolsMessageProcessor();
|
|
@@ -155,14 +155,26 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
155
155
|
* Check if content is already compressed
|
|
156
156
|
*/
|
|
157
157
|
private isAlreadyCompressed(content: string): boolean {
|
|
158
|
-
// Check for compressed
|
|
159
|
-
|
|
158
|
+
// Check for a compressed-string marker, but ONLY when the content *starts*
|
|
159
|
+
// with it. Compressor output always begins with "[COMPRESSED_STRING ...".
|
|
160
|
+
// A naive `.includes()` produced false positives for large payloads (e.g. a
|
|
161
|
+
// usage.json / message-history dump) that merely CONTAIN that substring deep
|
|
162
|
+
// inside their nested data — those files were left completely uncompressed
|
|
163
|
+
// because this guard incorrectly reported them as already compressed.
|
|
164
|
+
if (content.trimStart().startsWith("[COMPRESSED_STRING")) {
|
|
160
165
|
return true;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// Check for compressed JSON structure with schema key
|
|
169
|
+
// Only treat this as compressed when `_schema_key` is an actual TOP-LEVEL
|
|
170
|
+
// key of the parsed object — not merely present somewhere in the string.
|
|
164
171
|
const parsed = this.tryParseJson(content);
|
|
165
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
parsed &&
|
|
174
|
+
typeof parsed === "object" &&
|
|
175
|
+
!Array.isArray(parsed) &&
|
|
176
|
+
typeof parsed._schema_key === "string"
|
|
177
|
+
) {
|
|
166
178
|
return true;
|
|
167
179
|
}
|
|
168
180
|
|
|
@@ -296,7 +308,12 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
296
308
|
return async (originalMessages: Message[], modifiedMessages: Message[]) => {
|
|
297
309
|
// Find the index of the last assistant message so we only compress
|
|
298
310
|
// the most recent batch of tool responses (after the last agent output).
|
|
299
|
-
// This prevents re-compressing already-cached historical messages
|
|
311
|
+
// This prevents re-compressing already-cached historical messages, which
|
|
312
|
+
// would break prompt caching. Newly-added tool responses (the "current
|
|
313
|
+
// batch") are the ones that need compressing before they reach the model,
|
|
314
|
+
// and they appear after the last assistant message at the moment this
|
|
315
|
+
// processor runs (post_tools / pre_call), when the fresh tool response is
|
|
316
|
+
// still the tail of the message list.
|
|
300
317
|
let lastAssistantIndex = -1;
|
|
301
318
|
for (let i = modifiedMessages.length - 1; i >= 0; i--) {
|
|
302
319
|
if (modifiedMessages[i].role === "assistant") {
|
package/src/processors/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
export { Base64ImageProcessor } from "./Base64ImageDetector";
|
|
2
2
|
export { CustomVariables } from "./CustomVariables";
|
|
3
3
|
export { TokenCompressor } from "./TokenCompressor";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
JsonCompressor,
|
|
6
|
+
JsonSchema,
|
|
7
|
+
CompressionMetadata,
|
|
8
|
+
JsonCompressorStorage,
|
|
9
|
+
} from "./JsonCompressor";
|
|
5
10
|
export { ToolResponseCache } from "./ToolResponseCache";
|
|
6
11
|
export { XmlToolCallProcessor } from "./XmlToolCallProcessor";
|
|
7
12
|
export { HarmonyToolProcessor } from "./HarmonyToolProcessor";
|
|
13
|
+
export { MinimalToolsMessageProcessor } from "./MinimalToolsMessageProcessor";
|
|
@@ -33,6 +33,7 @@ export class AgentSyncFs {
|
|
|
33
33
|
private agent: BaseAgent | undefined;
|
|
34
34
|
private threadUpdateHandler: ((...args: any[]) => void) | undefined;
|
|
35
35
|
private doneHandler: ((...args: any[]) => void) | undefined;
|
|
36
|
+
private tokenUsageHandler: ((...args: any[]) => void) | undefined;
|
|
36
37
|
/**
|
|
37
38
|
* Tracks the most recent in-flight filesystem metadata update.
|
|
38
39
|
* The done handler awaits this before finalizing, preventing a race where
|
|
@@ -118,6 +119,33 @@ export class AgentSyncFs {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Append a token usage entry (including the full message chain sent for
|
|
124
|
+
* that call) to usage.json. This is intended for debugging cache-hit
|
|
125
|
+
* issues, where comparing the message chains and usage values between
|
|
126
|
+
* consecutive calls can reveal when a cache prefix was invalidated.
|
|
127
|
+
*/
|
|
128
|
+
private async appendUsageEntry(entry: any): Promise<void> {
|
|
129
|
+
if (!this.taskPath) return;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const usagePath = path.join(this.taskPath, "usage.json");
|
|
133
|
+
let entries: any[] = [];
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const existingData = await fs.readFile(usagePath, "utf8");
|
|
137
|
+
entries = JSON.parse(existingData);
|
|
138
|
+
} catch {
|
|
139
|
+
// File doesn't exist or is invalid, start fresh
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
entries.push(entry);
|
|
143
|
+
await fs.writeFile(usagePath, JSON.stringify(entries, null, 2), "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`❌ Failed to append usage entry:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
/**
|
|
122
150
|
* Update metadata file with current agent state
|
|
123
151
|
*/
|
|
@@ -352,6 +380,20 @@ export class AgentSyncFs {
|
|
|
352
380
|
this.threadUpdateHandler
|
|
353
381
|
);
|
|
354
382
|
|
|
383
|
+
// Listen to token usage events to record per-call usage + message chain
|
|
384
|
+
// (store reference for cleanup). This is written to usage.json so that
|
|
385
|
+
// cache-hit/cache-miss issues can be debugged by comparing the message
|
|
386
|
+
// chain and usage values sent/received on consecutive AI calls.
|
|
387
|
+
this.tokenUsageHandler = (usage: any) => {
|
|
388
|
+
this.appendUsageEntry(usage).catch((error) => {
|
|
389
|
+
console.error(`❌ Error recording token usage:`, error);
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
agent.agentEvents.on(
|
|
393
|
+
agent.eventTypes.tokenUsage,
|
|
394
|
+
this.tokenUsageHandler
|
|
395
|
+
);
|
|
396
|
+
|
|
355
397
|
// Listen to completion event to finalize task (store reference for cleanup)
|
|
356
398
|
this.doneHandler = (result: string) => {
|
|
357
399
|
if (!this.taskId) {
|
|
@@ -495,6 +537,13 @@ export class AgentSyncFs {
|
|
|
495
537
|
);
|
|
496
538
|
this.threadUpdateHandler = undefined;
|
|
497
539
|
}
|
|
540
|
+
if (this.tokenUsageHandler) {
|
|
541
|
+
this.agent.agentEvents.removeListener(
|
|
542
|
+
this.agent.eventTypes.tokenUsage,
|
|
543
|
+
this.tokenUsageHandler
|
|
544
|
+
);
|
|
545
|
+
this.tokenUsageHandler = undefined;
|
|
546
|
+
}
|
|
498
547
|
if (this.doneHandler) {
|
|
499
548
|
this.agent.agentEvents.removeListener(
|
|
500
549
|
this.agent.eventTypes.done,
|
|
@@ -89,7 +89,7 @@ export class AgentSyncKnowhowWeb {
|
|
|
89
89
|
try {
|
|
90
90
|
await this.client.updateChatTask(knowhowTaskId, {
|
|
91
91
|
threads: agent.getThreads(),
|
|
92
|
-
|
|
92
|
+
totalUsdCost: agent.getTotalCostUsd(),
|
|
93
93
|
inProgress,
|
|
94
94
|
...(result ? { result } : {}),
|
|
95
95
|
});
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { KnowhowSimpleClient } from "./KnowhowClient";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { getConfig } from "../config";
|
|
7
|
+
|
|
8
|
+
const BEHAVIORS_DIR = ".knowhow/behaviors";
|
|
9
|
+
const LOCAL_SKILLS_DIRS = [".agents/skills", ".knowhow/skills"];
|
|
10
|
+
const GLOBAL_SKILLS_DIR = path.join(os.homedir(), ".knowhow", "skills");
|
|
11
|
+
|
|
12
|
+
export interface Behavior {
|
|
13
|
+
id?: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
instructions: string;
|
|
17
|
+
textTrigger?: string;
|
|
18
|
+
semanticTriggerText?: string;
|
|
19
|
+
mcpServers?: string;
|
|
20
|
+
tools?: string;
|
|
21
|
+
embeddings?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
toolsDisabled?: boolean;
|
|
24
|
+
isPublic?: boolean;
|
|
25
|
+
isSkill?: boolean;
|
|
26
|
+
platform?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* In-memory singleton for behaviors/skills.
|
|
31
|
+
* Call initFromDisk() once at startup; tools then read from memory.
|
|
32
|
+
*/
|
|
33
|
+
export class BehaviorsService {
|
|
34
|
+
private behaviors: Behavior[] = [];
|
|
35
|
+
private skills: Behavior[] = [];
|
|
36
|
+
public client = new KnowhowSimpleClient();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load behaviors (and derive skills) from disk into memory.
|
|
40
|
+
* @param filePaths - optional explicit file paths to load; defaults to all files in BEHAVIORS_DIR
|
|
41
|
+
*/
|
|
42
|
+
initFromDisk(filePaths?: string[]): void {
|
|
43
|
+
if (filePaths && filePaths.length > 0) {
|
|
44
|
+
const loaded: Behavior[] = [];
|
|
45
|
+
for (const fp of filePaths) {
|
|
46
|
+
try {
|
|
47
|
+
const raw = fs.readFileSync(fp, "utf-8");
|
|
48
|
+
const behavior = fp.endsWith(".md")
|
|
49
|
+
? this.parseMdBehavior(raw)
|
|
50
|
+
: (JSON.parse(raw) as Behavior);
|
|
51
|
+
loaded.push(behavior);
|
|
52
|
+
} catch {
|
|
53
|
+
// skip malformed files
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.behaviors = loaded;
|
|
57
|
+
} else {
|
|
58
|
+
this.behaviors = this.loadLocal();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.skills = this.behaviors.filter((b) => b.isSkill === true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async initFromConfig() {
|
|
65
|
+
// Load skills/behaviors from disk into memory (respects config.skills file list if set)
|
|
66
|
+
let config: { skills?: string[] } = {};
|
|
67
|
+
try {
|
|
68
|
+
config = await getConfig();
|
|
69
|
+
} catch {
|
|
70
|
+
/* no config file */
|
|
71
|
+
}
|
|
72
|
+
const skills = config.skills || [];
|
|
73
|
+
this.initFromDisk(skills);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Initialize only skills subset into memory. */
|
|
77
|
+
initSkills(skills: Behavior[]): void {
|
|
78
|
+
this.skills = skills;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
initBehaviors(behaviors: Behavior[]): void {
|
|
82
|
+
this.behaviors = behaviors;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Return all in-memory behaviors. */
|
|
86
|
+
getBehaviors(): Behavior[] {
|
|
87
|
+
return this.behaviors;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Return all in-memory skills (behaviors where isSkill === true). */
|
|
91
|
+
listSkills(): Behavior[] {
|
|
92
|
+
return this.skills;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Find a skill by name (exact then partial match). Returns undefined if not found. */
|
|
96
|
+
findSkill(name: string): Behavior | undefined {
|
|
97
|
+
const lower = name.toLowerCase();
|
|
98
|
+
return (
|
|
99
|
+
this.skills.find((s) => s.name.toLowerCase() === lower) ??
|
|
100
|
+
this.skills.find(
|
|
101
|
+
(s) =>
|
|
102
|
+
s.name.toLowerCase().includes(lower) ||
|
|
103
|
+
lower.includes(s.name.toLowerCase())
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Singleton instance — import this everywhere instead of constructing BehaviorService directly. */
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load all behaviors from local disk (.knowhow/behaviors/).
|
|
112
|
+
* Supports both .json and .md files.
|
|
113
|
+
* Also scans .agents/skills/ and .knowhow/skills/ for SKILL.md files,
|
|
114
|
+
* treating them as skills (isSkill: true) regardless of frontmatter.
|
|
115
|
+
*/
|
|
116
|
+
loadLocal(opts: { skillsOnly?: boolean } = {}): Behavior[] {
|
|
117
|
+
const results: Behavior[] = [];
|
|
118
|
+
|
|
119
|
+
// Load from .knowhow/behaviors/ (existing behaviors/skills with explicit isSkill flag)
|
|
120
|
+
if (fs.existsSync(BEHAVIORS_DIR)) {
|
|
121
|
+
const files = fs
|
|
122
|
+
.readdirSync(BEHAVIORS_DIR)
|
|
123
|
+
.filter((f) => f.endsWith(".json") || f.endsWith(".md"));
|
|
124
|
+
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
try {
|
|
127
|
+
const filePath = path.join(BEHAVIORS_DIR, file);
|
|
128
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
129
|
+
const behavior = file.endsWith(".md")
|
|
130
|
+
? this.parseMdBehavior(raw)
|
|
131
|
+
: (JSON.parse(raw) as Behavior);
|
|
132
|
+
if (opts.skillsOnly && !behavior.isSkill) continue;
|
|
133
|
+
results.push(behavior);
|
|
134
|
+
} catch {
|
|
135
|
+
// skip malformed files
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Load from .agents/skills/<name>/SKILL.md and .knowhow/skills/<name>/SKILL.md
|
|
141
|
+
// Also load from global ~/.knowhow/skills/<name>/SKILL.md
|
|
142
|
+
// Files here are always treated as skills (isSkill: true)
|
|
143
|
+
const allSkillsDirs = [
|
|
144
|
+
...LOCAL_SKILLS_DIRS,
|
|
145
|
+
GLOBAL_SKILLS_DIR,
|
|
146
|
+
];
|
|
147
|
+
for (const skillsDir of allSkillsDirs) {
|
|
148
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
149
|
+
|
|
150
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory()) continue;
|
|
153
|
+
const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
|
|
154
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
155
|
+
try {
|
|
156
|
+
const raw = fs.readFileSync(skillFile, "utf-8");
|
|
157
|
+
const behavior = this.parseMdBehavior(raw, { isSkill: true });
|
|
158
|
+
results.push(behavior);
|
|
159
|
+
} catch {
|
|
160
|
+
// skip malformed files
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Match a behavior from local disk by textTrigger or name prefix.
|
|
170
|
+
* Does NOT hit the network.
|
|
171
|
+
*/
|
|
172
|
+
matchBehaviorLocal(userInput: string): Behavior | null {
|
|
173
|
+
const behaviors = this.loadLocal();
|
|
174
|
+
const lower = userInput.toLowerCase();
|
|
175
|
+
|
|
176
|
+
// Try exact text trigger match first
|
|
177
|
+
for (const b of behaviors) {
|
|
178
|
+
if (b.textTrigger && lower.startsWith(b.textTrigger.toLowerCase())) {
|
|
179
|
+
return b;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try partial text trigger match
|
|
184
|
+
for (const b of behaviors) {
|
|
185
|
+
if (b.textTrigger && lower.includes(b.textTrigger.toLowerCase())) {
|
|
186
|
+
return b;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async list(opts: { skillsOnly?: boolean; includeInternal?: boolean } = {}) {
|
|
194
|
+
const behaviors = opts.skillsOnly
|
|
195
|
+
? await this.client.getOrgSkills(opts.includeInternal ?? true)
|
|
196
|
+
: await this.client.getOrgBehaviors();
|
|
197
|
+
|
|
198
|
+
const items = behaviors.data;
|
|
199
|
+
if (items.length === 0) {
|
|
200
|
+
console.log("No behaviors found.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const b of items) {
|
|
205
|
+
const type = b.isSkill ? "[skill]" : "[behavior]";
|
|
206
|
+
const platformLabel = b.platform ? " [platform]" : "";
|
|
207
|
+
console.log(`${type}${platformLabel} ${b.name} — ${b.description || ""}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async download(
|
|
212
|
+
opts: {
|
|
213
|
+
skillsOnly?: boolean;
|
|
214
|
+
includeInternal?: boolean;
|
|
215
|
+
md?: boolean;
|
|
216
|
+
} = {}
|
|
217
|
+
) {
|
|
218
|
+
fs.mkdirSync(BEHAVIORS_DIR, { recursive: true });
|
|
219
|
+
|
|
220
|
+
const behaviors = opts.skillsOnly
|
|
221
|
+
? await this.client.getOrgSkills(opts.includeInternal ?? true)
|
|
222
|
+
: await this.client.getOrgBehaviors();
|
|
223
|
+
|
|
224
|
+
const items = behaviors.data;
|
|
225
|
+
let created = 0;
|
|
226
|
+
let updated = 0;
|
|
227
|
+
let unchanged = 0;
|
|
228
|
+
|
|
229
|
+
for (const b of items) {
|
|
230
|
+
const name = (b.name as string) || "unnamed";
|
|
231
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
232
|
+
const filePath = path.join(
|
|
233
|
+
BEHAVIORS_DIR,
|
|
234
|
+
`${slug}.${opts.md ? "md" : "json"}`
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (opts.md) {
|
|
238
|
+
const frontmatterLines: string[] = [
|
|
239
|
+
"---",
|
|
240
|
+
`name: ${name}`,
|
|
241
|
+
`description: ${(b.description as string) || ""}`,
|
|
242
|
+
];
|
|
243
|
+
if (b.model) frontmatterLines.push(`model: ${b.model}`);
|
|
244
|
+
if (b.textTrigger)
|
|
245
|
+
frontmatterLines.push(`textTrigger: ${b.textTrigger}`);
|
|
246
|
+
if (b.semanticTriggerText)
|
|
247
|
+
frontmatterLines.push(
|
|
248
|
+
`semanticTriggerText: ${b.semanticTriggerText}`
|
|
249
|
+
);
|
|
250
|
+
if (b.isSkill !== undefined)
|
|
251
|
+
frontmatterLines.push(`isSkill: ${b.isSkill}`);
|
|
252
|
+
if (b.isPublic !== undefined)
|
|
253
|
+
frontmatterLines.push(`isPublic: ${b.isPublic}`);
|
|
254
|
+
if (b.mcpServers) frontmatterLines.push(`mcpServers: ${b.mcpServers}`);
|
|
255
|
+
if (b.tools) frontmatterLines.push(`tools: ${b.tools}`);
|
|
256
|
+
if (b.embeddings) frontmatterLines.push(`embeddings: ${b.embeddings}`);
|
|
257
|
+
frontmatterLines.push(`id: ${b.id}`);
|
|
258
|
+
frontmatterLines.push("---");
|
|
259
|
+
|
|
260
|
+
const content = `${frontmatterLines.join("\n")}\n\n${
|
|
261
|
+
(b.instructions as string) || ""
|
|
262
|
+
}`;
|
|
263
|
+
const changed = this.writeIfChanged(filePath, content);
|
|
264
|
+
if (changed === "created") {
|
|
265
|
+
created++;
|
|
266
|
+
console.log(`✨ Created: ${filePath}`);
|
|
267
|
+
} else if (changed === "updated") {
|
|
268
|
+
updated++;
|
|
269
|
+
console.log(`📝 Updated: ${filePath}`);
|
|
270
|
+
} else {
|
|
271
|
+
unchanged++;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
const data: Behavior = {
|
|
275
|
+
id: b.id as string,
|
|
276
|
+
name,
|
|
277
|
+
description: (b.description as string) || "",
|
|
278
|
+
instructions: (b.instructions as string) || "",
|
|
279
|
+
textTrigger: b.textTrigger as string | undefined,
|
|
280
|
+
semanticTriggerText: b.semanticTriggerText as string | undefined,
|
|
281
|
+
mcpServers: b.mcpServers as string | undefined,
|
|
282
|
+
tools: b.tools as string | undefined,
|
|
283
|
+
embeddings: b.embeddings as string | undefined,
|
|
284
|
+
model: b.model as string | undefined,
|
|
285
|
+
toolsDisabled: b.toolsDisabled as boolean | undefined,
|
|
286
|
+
isPublic: b.isPublic as boolean | undefined,
|
|
287
|
+
isSkill: b.isSkill as boolean | undefined,
|
|
288
|
+
platform: b.platform as boolean | undefined,
|
|
289
|
+
};
|
|
290
|
+
const content = JSON.stringify(data, null, 2);
|
|
291
|
+
const changed = this.writeIfChanged(filePath, content);
|
|
292
|
+
if (changed === "created") {
|
|
293
|
+
created++;
|
|
294
|
+
console.log(`✨ Created: ${filePath}`);
|
|
295
|
+
} else if (changed === "updated") {
|
|
296
|
+
updated++;
|
|
297
|
+
console.log(`📝 Updated: ${filePath}`);
|
|
298
|
+
} else {
|
|
299
|
+
unchanged++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(
|
|
305
|
+
`\nSync complete: ${created} created, ${updated} updated, ${unchanged} unchanged → ${BEHAVIORS_DIR}/`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Write content to filePath only if it differs from what's there already.
|
|
311
|
+
* Returns "created" | "updated" | "unchanged".
|
|
312
|
+
*/
|
|
313
|
+
private writeIfChanged(
|
|
314
|
+
filePath: string,
|
|
315
|
+
content: string
|
|
316
|
+
): "created" | "updated" | "unchanged" {
|
|
317
|
+
const hash = (s: string) => createHash("sha256").update(s).digest("hex");
|
|
318
|
+
|
|
319
|
+
if (!fs.existsSync(filePath)) {
|
|
320
|
+
fs.writeFileSync(filePath, content);
|
|
321
|
+
return "created";
|
|
322
|
+
}
|
|
323
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
324
|
+
if (hash(existing) !== hash(content)) {
|
|
325
|
+
fs.writeFileSync(filePath, content);
|
|
326
|
+
return "updated";
|
|
327
|
+
}
|
|
328
|
+
return "unchanged";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
parseMdBehavior(content: string, defaults: Partial<Behavior> = {}): Behavior {
|
|
332
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
333
|
+
if (!match)
|
|
334
|
+
throw new Error("Invalid behavior markdown: missing frontmatter");
|
|
335
|
+
|
|
336
|
+
const meta: Record<string, string> = {};
|
|
337
|
+
for (const line of match[1].split("\n")) {
|
|
338
|
+
const colonIdx = line.indexOf(":");
|
|
339
|
+
if (colonIdx > 0) {
|
|
340
|
+
const key = line.slice(0, colonIdx).trim();
|
|
341
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
342
|
+
meta[key] = val;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
id: meta.id,
|
|
348
|
+
name: meta.name || "",
|
|
349
|
+
description: meta.description || "",
|
|
350
|
+
instructions: match[2].trim(),
|
|
351
|
+
textTrigger: meta.textTrigger || undefined,
|
|
352
|
+
semanticTriggerText: meta.semanticTriggerText || undefined,
|
|
353
|
+
model: meta.model || undefined,
|
|
354
|
+
mcpServers: meta.mcpServers || undefined,
|
|
355
|
+
tools: meta.tools || undefined,
|
|
356
|
+
embeddings: meta.embeddings || undefined,
|
|
357
|
+
isSkill: meta.isSkill === "true" || defaults.isSkill === true,
|
|
358
|
+
isPublic: meta.isPublic === "true",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async upload() {
|
|
363
|
+
if (!fs.existsSync(BEHAVIORS_DIR)) {
|
|
364
|
+
console.error(
|
|
365
|
+
"No .knowhow/behaviors/ directory found. Run 'knowhow behaviors download' first."
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const files = fs
|
|
371
|
+
.readdirSync(BEHAVIORS_DIR)
|
|
372
|
+
.filter((f) => f.endsWith(".json") || f.endsWith(".md"));
|
|
373
|
+
|
|
374
|
+
for (const file of files) {
|
|
375
|
+
const filePath = path.join(BEHAVIORS_DIR, file);
|
|
376
|
+
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
377
|
+
|
|
378
|
+
let data: Behavior;
|
|
379
|
+
if (file.endsWith(".md")) {
|
|
380
|
+
data = this.parseMdBehavior(rawContent);
|
|
381
|
+
} else {
|
|
382
|
+
data = JSON.parse(rawContent) as Behavior;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (data.id) {
|
|
386
|
+
await this.client.updateOrgBehavior(
|
|
387
|
+
data.id,
|
|
388
|
+
data as unknown as Record<string, unknown>
|
|
389
|
+
);
|
|
390
|
+
console.log(`Updated: ${data.name}`);
|
|
391
|
+
} else {
|
|
392
|
+
await this.client.createOrgBehavior(
|
|
393
|
+
data as unknown as Record<string, unknown>
|
|
394
|
+
);
|
|
395
|
+
console.log(`Created: ${data.name}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log(`Uploaded ${files.length} behaviors.`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Try to match a behavior by text trigger.
|
|
404
|
+
* Returns the first matched behavior or null.
|
|
405
|
+
*/
|
|
406
|
+
async matchBehavior(userInput: string): Promise<Behavior | null> {
|
|
407
|
+
try {
|
|
408
|
+
const result = await this.client.getTriggeredBehaviors(userInput);
|
|
409
|
+
const triggered = result.data.triggered;
|
|
410
|
+
if (triggered && triggered.length > 0) {
|
|
411
|
+
const b = triggered[0];
|
|
412
|
+
return {
|
|
413
|
+
id: b.id as string,
|
|
414
|
+
name: (b.name as string) || "",
|
|
415
|
+
description: (b.description as string) || "",
|
|
416
|
+
instructions: (b.instructions as string) || "",
|
|
417
|
+
textTrigger: b.textTrigger as string | undefined,
|
|
418
|
+
semanticTriggerText: b.semanticTriggerText as string | undefined,
|
|
419
|
+
model: b.model as string | undefined,
|
|
420
|
+
mcpServers: b.mcpServers as string | undefined,
|
|
421
|
+
tools: b.tools as string | undefined,
|
|
422
|
+
embeddings: b.embeddings as string | undefined,
|
|
423
|
+
isSkill: b.isSkill as boolean | undefined,
|
|
424
|
+
isPublic: b.isPublic as boolean | undefined,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// Not authenticated or backend unavailable — skip behavior matching
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|