@tyvm/knowhow 0.0.144 → 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/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/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/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/x-home.png +0 -0
package/bin/knowhow.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node --no-node-snapshot
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
// re-exec
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
// knowhow CLI entry point.
|
|
4
|
+
//
|
|
5
|
+
// --no-node-snapshot is required for isolated-vm (used by executeScript).
|
|
6
|
+
// It is declared in the shebang above so that Node starts with it directly —
|
|
7
|
+
// no re-exec needed. This means the process stays as a direct child of the
|
|
8
|
+
// terminal (e.g. Ghostty) and inherits its macOS TCC (Screen Recording /
|
|
9
|
+
// Accessibility) permissions without needing to grant them to node itself.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
["--no-node-snapshot", "--enable-source-maps", cliEntrypoint, ...process.argv.slice(2)],
|
|
14
|
-
{
|
|
15
|
-
stdio: "inherit",
|
|
16
|
-
}
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
process.exit(result.status ?? 1);
|
|
20
|
-
}
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const cliJs = path.join(__dirname, "../ts_build/src/cli.js");
|
|
21
13
|
|
|
22
|
-
//
|
|
23
|
-
|
|
14
|
+
// cli.js only auto-runs main() when it is the entry module (require.main ===
|
|
15
|
+
// module). Since we're requiring it here, we call main() ourselves so the
|
|
16
|
+
// CLI actually runs and exits cleanly.
|
|
17
|
+
const cli = require(cliJs);
|
|
18
|
+
cli.main()
|
|
19
|
+
.catch((e) => {
|
|
20
|
+
console.error(e);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
})
|
|
23
|
+
.then(() => {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migration script: Compress usage.json files in .knowhow/processes/agents/
|
|
4
|
+
*
|
|
5
|
+
* The old format writes the full message thread with every usage entry, causing
|
|
6
|
+
* exponential file growth. This script applies the same compression logic as the
|
|
7
|
+
* new appendUsageEntry code: if entry[N].messages is a prefix of entry[N+1].messages,
|
|
8
|
+
* replace entry[N].messages with [{ PREV_CACHE_HIT: true }].
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/migrate-usage-json.js # dry run (shows space savings)
|
|
12
|
+
* node scripts/migrate-usage-json.js --migrate # actually migrates files
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
|
|
18
|
+
const AGENTS_DIR = path.join(
|
|
19
|
+
__dirname,
|
|
20
|
+
"../.knowhow/processes/agents"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const DRY_RUN = !process.argv.includes("--migrate");
|
|
24
|
+
|
|
25
|
+
if (DRY_RUN) {
|
|
26
|
+
console.log("=== DRY RUN (pass --migrate to actually migrate) ===\n");
|
|
27
|
+
} else {
|
|
28
|
+
console.log("=== MIGRATING FILES ===\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compressEntries(entries) {
|
|
32
|
+
let compressed = 0;
|
|
33
|
+
for (let i = 0; i < entries.length - 1; i++) {
|
|
34
|
+
const curr = entries[i];
|
|
35
|
+
const next = entries[i + 1];
|
|
36
|
+
const prevMessages = curr?.messages;
|
|
37
|
+
const nextMessages = next?.messages;
|
|
38
|
+
|
|
39
|
+
// Skip if already compressed
|
|
40
|
+
if (
|
|
41
|
+
Array.isArray(prevMessages) &&
|
|
42
|
+
prevMessages.length === 1 &&
|
|
43
|
+
prevMessages[0]?.PREV_CACHE_HIT
|
|
44
|
+
) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
Array.isArray(prevMessages) &&
|
|
50
|
+
Array.isArray(nextMessages) &&
|
|
51
|
+
prevMessages.length > 0 &&
|
|
52
|
+
nextMessages.length >= prevMessages.length &&
|
|
53
|
+
JSON.stringify(nextMessages.slice(0, prevMessages.length)) ===
|
|
54
|
+
JSON.stringify(prevMessages)
|
|
55
|
+
) {
|
|
56
|
+
curr.messages = [{ PREV_CACHE_HIT: true }];
|
|
57
|
+
compressed++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return compressed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let totalOriginalBytes = 0;
|
|
64
|
+
let totalNewBytes = 0;
|
|
65
|
+
let totalFilesProcessed = 0;
|
|
66
|
+
let totalFilesChanged = 0;
|
|
67
|
+
let totalEntriesCompressed = 0;
|
|
68
|
+
let errors = 0;
|
|
69
|
+
|
|
70
|
+
const agentDirs = fs.readdirSync(AGENTS_DIR);
|
|
71
|
+
|
|
72
|
+
for (const dir of agentDirs) {
|
|
73
|
+
const usagePath = path.join(AGENTS_DIR, dir, "usage.json");
|
|
74
|
+
|
|
75
|
+
if (!fs.existsSync(usagePath)) continue;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const originalContent = fs.readFileSync(usagePath, "utf8");
|
|
79
|
+
const originalBytes = Buffer.byteLength(originalContent, "utf8");
|
|
80
|
+
totalOriginalBytes += originalBytes;
|
|
81
|
+
totalFilesProcessed++;
|
|
82
|
+
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = JSON.parse(originalContent);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn(` [SKIP] ${dir}/usage.json - invalid JSON: ${e.message}`);
|
|
88
|
+
errors++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
93
|
+
totalNewBytes += originalBytes;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const compressed = compressEntries(entries);
|
|
98
|
+
totalEntriesCompressed += compressed;
|
|
99
|
+
|
|
100
|
+
const newContent = JSON.stringify(entries, null, 2);
|
|
101
|
+
const newBytes = Buffer.byteLength(newContent, "utf8");
|
|
102
|
+
totalNewBytes += newBytes;
|
|
103
|
+
|
|
104
|
+
const savedBytes = originalBytes - newBytes;
|
|
105
|
+
const savedPct = ((savedBytes / originalBytes) * 100).toFixed(1);
|
|
106
|
+
|
|
107
|
+
if (compressed > 0) {
|
|
108
|
+
totalFilesChanged++;
|
|
109
|
+
console.log(
|
|
110
|
+
` ${dir}/usage.json: ${compressed} entries compressed, ` +
|
|
111
|
+
`${(originalBytes / 1024).toFixed(1)}KB -> ${(newBytes / 1024).toFixed(1)}KB ` +
|
|
112
|
+
`(-${(savedBytes / 1024).toFixed(1)}KB, ${savedPct}%)`
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!DRY_RUN) {
|
|
116
|
+
fs.writeFileSync(usagePath, newContent, "utf8");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(` [ERROR] ${dir}: ${e.message}`);
|
|
121
|
+
errors++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const totalSaved = totalOriginalBytes - totalNewBytes;
|
|
126
|
+
const totalSavedPct = ((totalSaved / totalOriginalBytes) * 100).toFixed(1);
|
|
127
|
+
|
|
128
|
+
console.log("\n=== SUMMARY ===");
|
|
129
|
+
console.log(`Files found with usage.json: ${totalFilesProcessed}`);
|
|
130
|
+
console.log(`Files with compressible data: ${totalFilesChanged}`);
|
|
131
|
+
console.log(`Total entries compressed: ${totalEntriesCompressed}`);
|
|
132
|
+
console.log(
|
|
133
|
+
`Total size before: ${(totalOriginalBytes / 1024 / 1024).toFixed(2)} MB`
|
|
134
|
+
);
|
|
135
|
+
console.log(
|
|
136
|
+
`Total size after: ${(totalNewBytes / 1024 / 1024).toFixed(2)} MB`
|
|
137
|
+
);
|
|
138
|
+
console.log(
|
|
139
|
+
`Space saved: ${(totalSaved / 1024 / 1024).toFixed(2)} MB (${totalSavedPct}%)`
|
|
140
|
+
);
|
|
141
|
+
if (errors > 0) console.log(`Errors: ${errors}`);
|
|
142
|
+
|
|
143
|
+
if (DRY_RUN && totalFilesChanged > 0) {
|
|
144
|
+
console.log(
|
|
145
|
+
"\nRun with --migrate to apply changes (backups written as .bak files)."
|
|
146
|
+
);
|
|
147
|
+
}
|
package/src/agents/base/base.ts
CHANGED
|
@@ -43,6 +43,11 @@ export interface ToolCallEvent {
|
|
|
43
43
|
functionResp: any;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
type ChatCompletion = Parameters<GenericClient["createChatCompletion"]>[0];
|
|
47
|
+
type CachedChatCompletion = { messages: ChatCompletion["messages"] } & Partial<
|
|
48
|
+
Omit<ChatCompletion, "messages">
|
|
49
|
+
>;
|
|
50
|
+
|
|
46
51
|
/**
|
|
47
52
|
* A source is anything that can (a) start producing data, (b) hand each datum to
|
|
48
53
|
* a callback, and (c) be torn down. This is the unified contract behind
|
|
@@ -145,6 +150,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
145
150
|
protected taskBreakdown = "";
|
|
146
151
|
protected summaries = [] as string[];
|
|
147
152
|
protected currentTaskId: string | null = null;
|
|
153
|
+
/**
|
|
154
|
+
* The taskId of the parent agent that spawned this task, if any.
|
|
155
|
+
* Used by self-referential tools (e.g. replyToParent) to know who to
|
|
156
|
+
* report back to without relying solely on reading metadata.json off disk.
|
|
157
|
+
*/
|
|
158
|
+
public parentTaskId: string | null = null;
|
|
148
159
|
|
|
149
160
|
public agentEvents = new EventService();
|
|
150
161
|
public eventTypes = {
|
|
@@ -349,6 +360,14 @@ export abstract class BaseAgent implements IAgent {
|
|
|
349
360
|
return this.provider;
|
|
350
361
|
}
|
|
351
362
|
|
|
363
|
+
getParentTaskId(): string | null {
|
|
364
|
+
return this.parentTaskId;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
setParentTaskId(value: string | null) {
|
|
368
|
+
this.parentTaskId = value ?? null;
|
|
369
|
+
}
|
|
370
|
+
|
|
352
371
|
setProvider(value: keyof typeof Clients.clients) {
|
|
353
372
|
this.provider = value;
|
|
354
373
|
this.client = null; // Reset client to force re-fetch
|
|
@@ -595,14 +614,31 @@ export abstract class BaseAgent implements IAgent {
|
|
|
595
614
|
* no-op).
|
|
596
615
|
*/
|
|
597
616
|
protected async createAgentCompletion(
|
|
598
|
-
params:
|
|
617
|
+
params: CachedChatCompletion,
|
|
599
618
|
options: {
|
|
600
619
|
interruptValue?: CompletionResponse;
|
|
601
620
|
} = {}
|
|
602
621
|
): Promise<CompletionResponse> {
|
|
603
622
|
const { interruptValue } = options;
|
|
604
623
|
|
|
605
|
-
|
|
624
|
+
// If you change the tools, or any of this, you bust cache
|
|
625
|
+
// only safe to change messages
|
|
626
|
+
const defaultedParams: ChatCompletion = {
|
|
627
|
+
model: this.getModel(),
|
|
628
|
+
tools: this.getEnabledTools(),
|
|
629
|
+
tool_choice: "auto",
|
|
630
|
+
long_ttl_cache: this.runTime() > 300_000,
|
|
631
|
+
...(this.reasoningEffort !== undefined && {
|
|
632
|
+
reasoning_effort: this.reasoningEffort,
|
|
633
|
+
}),
|
|
634
|
+
...(this.summarizeReasoning !== undefined && {
|
|
635
|
+
reasoning_summary: this.summarizeReasoning,
|
|
636
|
+
}),
|
|
637
|
+
|
|
638
|
+
...params,
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const callPromise = this.getClient().createChatCompletion(defaultedParams);
|
|
606
642
|
const response = interruptValue
|
|
607
643
|
? await this.makeInterruptible(callPromise, interruptValue)
|
|
608
644
|
: await callPromise;
|
|
@@ -663,6 +699,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
663
699
|
this.tools.callTool(toolCall, this.getEnabledToolNames(), {
|
|
664
700
|
caller: this,
|
|
665
701
|
taskId: this.currentTaskId,
|
|
702
|
+
parentTaskId: this.parentTaskId ?? undefined,
|
|
666
703
|
}),
|
|
667
704
|
interruptResult
|
|
668
705
|
);
|
|
@@ -677,6 +714,14 @@ export abstract class BaseAgent implements IAgent {
|
|
|
677
714
|
|
|
678
715
|
logMessages(messages: Message[]) {
|
|
679
716
|
for (const message of messages) {
|
|
717
|
+
// Surface the model's reasoning summary (what it's "thinking") when the
|
|
718
|
+
// provider exposes one, so it appears alongside the assistant's output.
|
|
719
|
+
const reasoningSummary = (message as any).reasoning_summary;
|
|
720
|
+
if (message.role === "assistant" && reasoningSummary) {
|
|
721
|
+
this.agentEvents.emit(this.eventTypes.agentSay, {
|
|
722
|
+
message: `💭 ${reasoningSummary}`,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
680
725
|
if (message.role === "assistant" && message.content) {
|
|
681
726
|
this.agentEvents.emit(this.eventTypes.agentSay, {
|
|
682
727
|
message: message.content,
|
|
@@ -901,10 +946,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
901
946
|
// would pre-empt the next tool call and drop its result. Any message
|
|
902
947
|
// supplied with the poke was already added as a pending user message
|
|
903
948
|
// above, so the agent will still see it on its next step.
|
|
904
|
-
this.log(
|
|
905
|
-
"No active interruptible operation to interrupt",
|
|
906
|
-
"warn"
|
|
907
|
-
);
|
|
949
|
+
this.log("No active interruptible operation to interrupt", "warn");
|
|
908
950
|
}
|
|
909
951
|
}
|
|
910
952
|
|
|
@@ -1435,12 +1477,18 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1435
1477
|
};
|
|
1436
1478
|
|
|
1437
1479
|
const teardownPromise = Promise.resolve(source.start(emit)).catch((e) => {
|
|
1438
|
-
this.log(
|
|
1480
|
+
this.log(
|
|
1481
|
+
`observe(${source.label}) start failed: ${e?.message ?? e}`,
|
|
1482
|
+
"warn"
|
|
1483
|
+
);
|
|
1439
1484
|
return undefined;
|
|
1440
1485
|
});
|
|
1441
1486
|
|
|
1442
1487
|
const expiry = maxDurationMs
|
|
1443
|
-
? setTimeout(
|
|
1488
|
+
? setTimeout(
|
|
1489
|
+
() => this.stopObserving(id, "max duration reached"),
|
|
1490
|
+
maxDurationMs
|
|
1491
|
+
)
|
|
1444
1492
|
: undefined;
|
|
1445
1493
|
if (expiry && typeof (expiry as any).unref === "function") {
|
|
1446
1494
|
(expiry as any).unref();
|
|
@@ -1601,7 +1649,15 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1601
1649
|
const response = await this.createAgentCompletion({
|
|
1602
1650
|
model,
|
|
1603
1651
|
messages: taskBreakdownMessages,
|
|
1604
|
-
|
|
1652
|
+
tools: this.getEnabledTools(),
|
|
1653
|
+
tool_choice: "auto",
|
|
1654
|
+
long_ttl_cache: this.runTime() > 300_000,
|
|
1655
|
+
...(this.reasoningEffort !== undefined && {
|
|
1656
|
+
reasoning_effort: this.reasoningEffort,
|
|
1657
|
+
}),
|
|
1658
|
+
...(this.summarizeReasoning !== undefined && {
|
|
1659
|
+
reasoning_summary: this.summarizeReasoning,
|
|
1660
|
+
}),
|
|
1605
1661
|
});
|
|
1606
1662
|
|
|
1607
1663
|
this.taskBreakdown = response.choices[0].message.content;
|
|
@@ -1644,7 +1700,6 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1644
1700
|
] as Message[];
|
|
1645
1701
|
|
|
1646
1702
|
const response = await this.createAgentCompletion({
|
|
1647
|
-
model,
|
|
1648
1703
|
messages: compressMessagesPayload,
|
|
1649
1704
|
});
|
|
1650
1705
|
|
|
@@ -907,6 +907,46 @@ export function fixPatch(originalContent: string, patch: string): string {
|
|
|
907
907
|
return finalPatch;
|
|
908
908
|
}
|
|
909
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Verify that auto-correction did not broaden or change the requested edit.
|
|
912
|
+
* Line locations and hunk headers may change, but the exact multiset of added
|
|
913
|
+
* and removed lines must remain the same.
|
|
914
|
+
*/
|
|
915
|
+
export function autoCorrectionPreservesChanges(
|
|
916
|
+
originalContent: string,
|
|
917
|
+
updatedContent: string,
|
|
918
|
+
requestedPatch: string
|
|
919
|
+
): boolean {
|
|
920
|
+
const changedLines = (patch: string, prefix: "+" | "-"): string[] =>
|
|
921
|
+
parseHunks(patch)
|
|
922
|
+
.flatMap((hunk) =>
|
|
923
|
+
hunk.lines
|
|
924
|
+
.filter((line) => line.startsWith(prefix))
|
|
925
|
+
.map((line) => line.slice(1))
|
|
926
|
+
)
|
|
927
|
+
.sort();
|
|
928
|
+
|
|
929
|
+
const actualPatch = createTwoFilesPatch(
|
|
930
|
+
"original",
|
|
931
|
+
"updated",
|
|
932
|
+
originalContent,
|
|
933
|
+
updatedContent,
|
|
934
|
+
"",
|
|
935
|
+
"",
|
|
936
|
+
{ context: 0 }
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
const requestedAdditions = changedLines(requestedPatch, "+");
|
|
940
|
+
const requestedRemovals = changedLines(requestedPatch, "-");
|
|
941
|
+
const actualAdditions = changedLines(actualPatch, "+");
|
|
942
|
+
const actualRemovals = changedLines(actualPatch, "-");
|
|
943
|
+
|
|
944
|
+
return (
|
|
945
|
+
JSON.stringify(actualAdditions) === JSON.stringify(requestedAdditions) &&
|
|
946
|
+
JSON.stringify(actualRemovals) === JSON.stringify(requestedRemovals)
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
910
950
|
// --- Existing Application/Utility Code (Keep as is, ensure imports/exports are correct) ---
|
|
911
951
|
|
|
912
952
|
export async function savePatchError(
|
|
@@ -976,9 +1016,32 @@ export async function patchFile(
|
|
|
976
1016
|
originalContent = await readFile(filePath, "utf8"); // Use async read
|
|
977
1017
|
}
|
|
978
1018
|
|
|
979
|
-
let updatedContent = applyPatch(originalContent, patch);
|
|
980
|
-
let appliedPatch = patch; // Keep track of which patch succeeded
|
|
981
1019
|
const patchHunks = parseHunks(patch);
|
|
1020
|
+
// `diff.applyPatch` can report success for malformed hunks whose declared
|
|
1021
|
+
// line counts disagree with their bodies. In that case it consumes the
|
|
1022
|
+
// wrong number of lines and can move additions across scope boundaries.
|
|
1023
|
+
// Send those hunks through our re-anchoring logic instead of trusting the
|
|
1024
|
+
// library's successful-but-corrupt result.
|
|
1025
|
+
const hasInvalidHunkCounts = patchHunks.some((hunk) => {
|
|
1026
|
+
const originalBodyCount = hunk.lines.filter(
|
|
1027
|
+
(line) =>
|
|
1028
|
+
!line.startsWith("+") &&
|
|
1029
|
+
line !== "\"
|
|
1030
|
+
).length;
|
|
1031
|
+
const newBodyCount = hunk.lines.filter(
|
|
1032
|
+
(line) =>
|
|
1033
|
+
!line.startsWith("-") &&
|
|
1034
|
+
line !== "\"
|
|
1035
|
+
).length;
|
|
1036
|
+
return (
|
|
1037
|
+
originalBodyCount !== hunk.originalLineCount ||
|
|
1038
|
+
newBodyCount !== hunk.newLineCount
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
let updatedContent = hasInvalidHunkCounts
|
|
1042
|
+
? false
|
|
1043
|
+
: applyPatch(originalContent, patch);
|
|
1044
|
+
let appliedPatch = patch; // Keep track of which patch succeeded
|
|
982
1045
|
const patchHasAdditions = patchHunks.some((h) => h.additions.length > 0);
|
|
983
1046
|
const patchHasDeletions = patchHunks.some((h) => h.subtractions.length > 0);
|
|
984
1047
|
|
|
@@ -1113,6 +1176,25 @@ export async function patchFile(
|
|
|
1113
1176
|
|
|
1114
1177
|
const wasFixed = appliedPatch !== patch;
|
|
1115
1178
|
|
|
1179
|
+
// Auto-correction may re-anchor hunks, but it must never invent removals,
|
|
1180
|
+
// drop requested edits, or move lines by representing them as extra edits.
|
|
1181
|
+
// Reject before events or disk writes if its actual delta differs from the
|
|
1182
|
+
// submitted patch.
|
|
1183
|
+
if (
|
|
1184
|
+
wasFixed &&
|
|
1185
|
+
!autoCorrectionPreservesChanges(
|
|
1186
|
+
originalContent,
|
|
1187
|
+
updatedContent as string,
|
|
1188
|
+
patch
|
|
1189
|
+
)
|
|
1190
|
+
) {
|
|
1191
|
+
return [
|
|
1192
|
+
`❌ Patch failed: auto-correction changed the requested edit, so no changes were written.`,
|
|
1193
|
+
`File: ${filePath} (${originalContent.split("\n").length} lines)`,
|
|
1194
|
+
`Tip: Re-read the file and submit smaller hunks with unique surrounding context.`,
|
|
1195
|
+
].join("\n");
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1116
1198
|
const eventResults: any[] = [];
|
|
1117
1199
|
// Emit pre-edit blocking event
|
|
1118
1200
|
if (context.Events) {
|
|
@@ -1,10 +1,85 @@
|
|
|
1
1
|
import { getConfiguredEmbeddings } from "../../embeddings";
|
|
2
2
|
import { execAsync } from "./execCommand";
|
|
3
3
|
|
|
4
|
+
// Max number of matching lines (with full text) we show per file before we
|
|
5
|
+
// summarize the rest as a list of line numbers.
|
|
6
|
+
const PER_FILE_LINE_LIMIT = 5;
|
|
7
|
+
|
|
8
|
+
interface FileMatches {
|
|
9
|
+
file: string;
|
|
10
|
+
// [lineNumber, lineText]
|
|
11
|
+
lines: Array<[number, string]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse `ag --group --numbers` style output into per-file match groups.
|
|
16
|
+
* With --group, ag prints the file path on its own line, followed by
|
|
17
|
+
* `<lineNumber>:<text>` lines, then a blank line between files.
|
|
18
|
+
*/
|
|
19
|
+
function parseAgGroupedOutput(stdout: string): FileMatches[] {
|
|
20
|
+
const results: FileMatches[] = [];
|
|
21
|
+
let current: FileMatches | null = null;
|
|
22
|
+
|
|
23
|
+
for (const rawLine of stdout.split("\n")) {
|
|
24
|
+
if (rawLine === "") {
|
|
25
|
+
// Blank line separates file groups
|
|
26
|
+
current = null;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const numberMatch = rawLine.match(/^(\d+):([\s\S]*)$/);
|
|
31
|
+
if (numberMatch && current) {
|
|
32
|
+
current.lines.push([Number(numberMatch[1]), numberMatch[2]]);
|
|
33
|
+
} else {
|
|
34
|
+
// Treat as a file header line
|
|
35
|
+
current = { file: rawLine, lines: [] };
|
|
36
|
+
results.push(current);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format the parsed matches into a helpful, readable report. Instead of ag's
|
|
45
|
+
* "Too many matches ... Skipping" error, we report the total match count, the
|
|
46
|
+
* line numbers, and cap the displayed lines at PER_FILE_LINE_LIMIT.
|
|
47
|
+
*/
|
|
48
|
+
function formatMatches(files: FileMatches[]): string {
|
|
49
|
+
if (files.length === 0) return "No matches found.";
|
|
50
|
+
|
|
51
|
+
const sections: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (const { file, lines } of files) {
|
|
54
|
+
const total = lines.length;
|
|
55
|
+
const shown = lines.slice(0, PER_FILE_LINE_LIMIT);
|
|
56
|
+
|
|
57
|
+
const header = `${file} (${total} match${total === 1 ? "" : "es"})`;
|
|
58
|
+
const body = shown
|
|
59
|
+
.map(([num, text]) => `${num}:${text}`)
|
|
60
|
+
.join("\n");
|
|
61
|
+
|
|
62
|
+
let section = `${header}\n${body}`;
|
|
63
|
+
|
|
64
|
+
if (total > PER_FILE_LINE_LIMIT) {
|
|
65
|
+
const remaining = lines.slice(PER_FILE_LINE_LIMIT);
|
|
66
|
+
const remainingLineNumbers = remaining.map(([num]) => num);
|
|
67
|
+
section +=
|
|
68
|
+
`\n... ${remaining.length} more match${
|
|
69
|
+
remaining.length === 1 ? "" : "es"
|
|
70
|
+
} (showing first ${PER_FILE_LINE_LIMIT} lines). ` +
|
|
71
|
+
`Remaining matches on lines: [${remainingLineNumbers.join(", ")}]`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sections.push(section);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return sections.join("\n\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
4
80
|
export async function textSearch(searchTerm) {
|
|
5
81
|
try {
|
|
6
82
|
// Escape the search term for safe shell usage
|
|
7
|
-
// Replace single quotes with '\'' which closes quote, adds escaped quote, reopens quote
|
|
8
83
|
// 1) Normalize whitespace (turn newlines/tabs into spaces)
|
|
9
84
|
const normalized = String(searchTerm)
|
|
10
85
|
.replace(/\r\n?/g, "\n") // normalize CRLF/CR → LF
|
|
@@ -16,8 +91,28 @@ export async function textSearch(searchTerm) {
|
|
|
16
91
|
// 2) Escape single quotes for safe single-quoted shell arg
|
|
17
92
|
const escapedTerm = normalized.replace(/'/g, "'\\''");
|
|
18
93
|
|
|
19
|
-
|
|
20
|
-
|
|
94
|
+
// Use --group + --numbers so we can parse per-file matches ourselves and
|
|
95
|
+
// produce a richer summary (counts + line numbers) instead of ag's
|
|
96
|
+
// unhelpful "Too many matches" error. We intentionally do NOT pass -m here.
|
|
97
|
+
const command = `ag --group --numbers -Q '${escapedTerm}'`;
|
|
98
|
+
const { stdout, stderr } = await execAsync(command);
|
|
99
|
+
|
|
100
|
+
const files = parseAgGroupedOutput(stdout || "");
|
|
101
|
+
const formatted = formatMatches(files);
|
|
102
|
+
|
|
103
|
+
// Preserve any non-"too many matches" stderr (real warnings/errors), but
|
|
104
|
+
// drop ag's per-file "Too many matches ... Skipping" noise since we now
|
|
105
|
+
// handle match limits ourselves.
|
|
106
|
+
const filteredStderr = (stderr || "")
|
|
107
|
+
.split("\n")
|
|
108
|
+
.filter((line) => !/Too many matches/i.test(line))
|
|
109
|
+
.join("\n")
|
|
110
|
+
.trim();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
stdout: formatted,
|
|
114
|
+
stderr: filteredStderr,
|
|
115
|
+
};
|
|
21
116
|
} catch (err) {
|
|
22
117
|
console.log(
|
|
23
118
|
"Falling back to embeddings text search since ag was not available"
|