@tyvm/knowhow 0.0.147 → 0.0.149
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 +1 -1
- package/src/agents/base/base.ts +45 -32
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +55 -27
- package/src/ai.ts +4 -1
- package/src/auth/keyAuth.ts +10 -4
- package/src/auth/registerIdentity.ts +67 -0
- package/src/chat/CliChatService.ts +23 -2
- package/src/chat/modules/AgentModule.ts +39 -7
- package/src/chat/modules/InternalChatModule.ts +5 -0
- package/src/chat/modules/ToolCommandModule.ts +102 -0
- package/src/chat/modules/index.ts +1 -0
- package/src/chat/types.ts +1 -1
- package/src/cli.ts +9 -2
- package/src/clients/http.ts +67 -7
- package/src/clients/index.ts +36 -1
- package/src/commands/agent.ts +1 -0
- package/src/commands/agents.ts +57 -9
- package/src/commands/misc.ts +17 -2
- package/src/commands/processes.ts +203 -0
- package/src/commands/services.ts +23 -1
- package/src/index.ts +1 -0
- package/src/processes/ProcessManager.ts +95 -0
- package/src/processes/managedProcessShim.ts +127 -0
- package/src/processors/Base64ImageDetector.ts +28 -4
- package/src/processors/JsonCompressor.ts +14 -0
- package/src/processors/TokenCompressor.ts +17 -14
- package/src/services/AgentService.ts +2 -2
- package/src/services/AgentSyncFs.ts +12 -1
- package/src/services/ExtensionsService.ts +25 -0
- package/src/services/LazyToolsService.ts +15 -20
- package/src/services/Tools.ts +10 -0
- package/src/services/index.ts +4 -0
- package/src/services/modules/computerUse.ts +3 -1
- package/src/services/modules/index.ts +6 -0
- package/src/services/modules/types.ts +4 -0
- package/tests/clients/AIClient.test.ts +98 -15
- package/tests/processes/managedProcessShim.test.ts +59 -0
- package/tests/processors/Base64ImageDetector.test.ts +21 -0
- package/tests/processors/TokenCompressor.test.ts +18 -0
- package/tests/unit/auth/registerIdentity.test.ts +93 -0
- package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
- package/tests/unit/commands/agents.test.ts +44 -1
- package/tests/unit/services/LazyToolsService.test.ts +24 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -1
- package/ts_build/src/agents/base/base.js +30 -21
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
- package/ts_build/src/agents/tools/execCommand.js +100 -223
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +2 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +14 -1
- package/ts_build/src/agents/tools/startAgentTask.js +37 -21
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +2 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/auth/keyAuth.js +10 -3
- package/ts_build/src/auth/keyAuth.js.map +1 -1
- package/ts_build/src/auth/registerIdentity.d.ts +7 -0
- package/ts_build/src/auth/registerIdentity.js +62 -0
- package/ts_build/src/auth/registerIdentity.js.map +1 -0
- package/ts_build/src/chat/CliChatService.d.ts +6 -0
- package/ts_build/src/chat/CliChatService.js +17 -2
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
- package/ts_build/src/chat/modules/AgentModule.js +27 -7
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
- package/ts_build/src/chat/modules/InternalChatModule.js +5 -0
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
- package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
- package/ts_build/src/chat/modules/index.d.ts +1 -0
- package/ts_build/src/chat/modules/index.js +3 -1
- package/ts_build/src/chat/modules/index.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +1 -1
- package/ts_build/src/cli.js +6 -2
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +4 -0
- package/ts_build/src/clients/http.js +68 -6
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.js +23 -1
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -0
- package/ts_build/src/commands/agents.js +43 -8
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +12 -1
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/processes.d.ts +2 -0
- package/ts_build/src/commands/processes.js +282 -0
- package/ts_build/src/commands/processes.js.map +1 -0
- package/ts_build/src/commands/services.js +11 -1
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +1 -0
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processes/ProcessManager.d.ts +51 -0
- package/ts_build/src/processes/ProcessManager.js +160 -0
- package/ts_build/src/processes/ProcessManager.js.map +1 -0
- package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
- package/ts_build/src/processes/managedProcessShim.js +167 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- package/ts_build/src/processors/Base64ImageDetector.d.ts +1 -0
- package/ts_build/src/processors/Base64ImageDetector.js +19 -2
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +7 -0
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +1 -1
- package/ts_build/src/processors/TokenCompressor.js +9 -8
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +1 -1
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.js +6 -1
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/ExtensionsService.d.ts +10 -0
- package/ts_build/src/services/ExtensionsService.js +17 -0
- package/ts_build/src/services/ExtensionsService.js.map +1 -0
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +5 -7
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +3 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +3 -0
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/computerUse.d.ts +3 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/index.js +5 -0
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +3 -0
- package/ts_build/tests/clients/AIClient.test.js +73 -11
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/processes/managedProcessShim.test.d.ts +1 -0
- package/ts_build/tests/processes/managedProcessShim.test.js +92 -0
- package/ts_build/tests/processes/managedProcessShim.test.js.map +1 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js +18 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +15 -0
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/unit/auth/registerIdentity.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js +86 -0
- package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
- package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
- package/ts_build/tests/unit/commands/agents.test.js +28 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -1
- package/ts_build/tests/unit/services/LazyToolsService.test.js +20 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -1
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -1035,9 +1035,6 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1035
1035
|
this.updateCurrentThread(messages);
|
|
1036
1036
|
const isMissingTool = this.isRequiredToolMissing();
|
|
1037
1037
|
|
|
1038
|
-
const startIndex = 0;
|
|
1039
|
-
const endIndex = messages.length;
|
|
1040
|
-
|
|
1041
1038
|
// Process messages before each AI call
|
|
1042
1039
|
messages = await this.messageProcessor.processMessages(
|
|
1043
1040
|
messages,
|
|
@@ -1283,14 +1280,14 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1283
1280
|
: `Compressing messages: ${contextTokens} tokens exceeds ${this.getCompressThreshold()}`
|
|
1284
1281
|
);
|
|
1285
1282
|
this._forceCompact = false;
|
|
1286
|
-
messages = await this.compressMessages(messages
|
|
1283
|
+
messages = await this.compressMessages(messages);
|
|
1287
1284
|
this.startNewThread(messages);
|
|
1288
1285
|
// Reset the real-token tracker. compressMessages() ran a completion over
|
|
1289
|
-
// the
|
|
1290
|
-
//
|
|
1291
|
-
// getContextTokenCount()
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1286
|
+
// the older history, so lastPromptTokens reflects the compression input
|
|
1287
|
+
// rather than the newly reconstructed thread. Zeroing it makes
|
|
1288
|
+
// getContextTokenCount() estimate the new thread until the next regular
|
|
1289
|
+
// completion reports fresh usage, preventing an immediate false
|
|
1290
|
+
// re-compaction on the following iteration.
|
|
1294
1291
|
this.lastPromptTokens = 0;
|
|
1295
1292
|
}
|
|
1296
1293
|
|
|
@@ -1673,8 +1670,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1673
1670
|
const response = await this.createAgentCompletion({
|
|
1674
1671
|
model,
|
|
1675
1672
|
messages: taskBreakdownMessages,
|
|
1676
|
-
|
|
1677
|
-
tool_choice: "auto",
|
|
1673
|
+
tool_choice: "none",
|
|
1678
1674
|
long_ttl_cache: this.runTime() > 300_000,
|
|
1679
1675
|
...(this.reasoningEffort !== undefined && {
|
|
1680
1676
|
reasoning_effort: this.reasoningEffort,
|
|
@@ -1684,20 +1680,37 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1684
1680
|
}),
|
|
1685
1681
|
});
|
|
1686
1682
|
|
|
1687
|
-
|
|
1683
|
+
// The model may return null content (e.g. when it responds with tool calls
|
|
1684
|
+
// instead of text). Guard against storing a literal null so that template
|
|
1685
|
+
// strings don't render "null".
|
|
1686
|
+
const breakdownContent = response.choices
|
|
1687
|
+
.map((c) => c.message.content)
|
|
1688
|
+
.filter((s): s is string => typeof s === "string" && s.trim().length > 0)
|
|
1689
|
+
.join("\n\n");
|
|
1690
|
+
|
|
1691
|
+
this.taskBreakdown = breakdownContent || "";
|
|
1688
1692
|
this.log(`task breakdown cost: ${response.usd_cost}`);
|
|
1689
|
-
return this.taskBreakdown;
|
|
1693
|
+
return this.taskBreakdown || undefined;
|
|
1690
1694
|
}
|
|
1691
1695
|
|
|
1692
|
-
async compressMessages(
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1696
|
+
async compressMessages(messages: Message[]) {
|
|
1697
|
+
// Preserve the latest agent interaction exactly. Starting the resumed thread
|
|
1698
|
+
// at the final assistant message keeps its tool calls, every tool response,
|
|
1699
|
+
// and any subsequent user messages together as one protocol-valid unit.
|
|
1700
|
+
let resumeIndex = messages.length - 1;
|
|
1701
|
+
while (
|
|
1702
|
+
resumeIndex >= 0 &&
|
|
1703
|
+
messages[resumeIndex].role !== "assistant"
|
|
1704
|
+
) {
|
|
1705
|
+
resumeIndex--;
|
|
1706
|
+
}
|
|
1707
|
+
const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
|
|
1708
|
+
const toCompress = messages.slice(0, compressionEnd);
|
|
1709
|
+
const resumeMessages = messages.slice(compressionEnd);
|
|
1710
|
+
|
|
1697
1711
|
this.log(
|
|
1698
|
-
`Compressing messages from ${
|
|
1712
|
+
`Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
|
|
1699
1713
|
);
|
|
1700
|
-
const toCompress = messages.slice(startIndex, endIndex);
|
|
1701
1714
|
const toCompressPrompt = `We are compressing our conversation to save memory.
|
|
1702
1715
|
Please summarize the conversation so far, so that we may continue the original task with a smaller context
|
|
1703
1716
|
|
|
@@ -1707,16 +1720,14 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1707
1720
|
3. Next Steps - what we're about to do next to continue the user's original request.
|
|
1708
1721
|
4. Tasks remaining - what tasks are left from the initial task breakdown.
|
|
1709
1722
|
|
|
1710
|
-
Our initial task breakdown: ${this.taskBreakdown}
|
|
1723
|
+
${this.taskBreakdown ? `Our initial task breakdown: ${this.taskBreakdown}` : ""}
|
|
1711
1724
|
|
|
1712
|
-
This summary will
|
|
1725
|
+
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1713
1726
|
|
|
1714
1727
|
`;
|
|
1715
1728
|
|
|
1716
|
-
const model = this.getModel();
|
|
1717
|
-
|
|
1718
1729
|
const compressMessagesPayload = [
|
|
1719
|
-
...
|
|
1730
|
+
...toCompress,
|
|
1720
1731
|
{
|
|
1721
1732
|
role: "user",
|
|
1722
1733
|
content: toCompressPrompt,
|
|
@@ -1725,31 +1736,33 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1725
1736
|
|
|
1726
1737
|
const response = await this.createAgentCompletion({
|
|
1727
1738
|
messages: compressMessagesPayload,
|
|
1739
|
+
tool_choice: "none",
|
|
1728
1740
|
});
|
|
1729
1741
|
|
|
1730
|
-
const summaries = response.choices
|
|
1742
|
+
const summaries = response.choices
|
|
1743
|
+
.map((c) => c.message.content)
|
|
1744
|
+
.filter((s): s is string => typeof s === "string" && s.trim().length > 0);
|
|
1731
1745
|
this.summaries.push(...summaries);
|
|
1732
1746
|
|
|
1733
1747
|
const startMessages = [
|
|
1734
1748
|
{
|
|
1735
1749
|
role: "user",
|
|
1736
1750
|
content: `
|
|
1737
|
-
Initial task breakdown:
|
|
1738
|
-
${this.taskBreakdown}
|
|
1751
|
+
${this.taskBreakdown ? `Initial task breakdown:\n ${this.taskBreakdown}` : "(No task breakdown available — summarize what you know from context above)"}
|
|
1739
1752
|
|
|
1740
1753
|
We have just compressed the conversation to save memory:
|
|
1741
|
-
${JSON.stringify(summaries)}
|
|
1754
|
+
${summaries.length > 0 ? JSON.stringify(summaries) : "(summary unavailable — please continue from the task breakdown above)"}
|
|
1742
1755
|
|
|
1743
1756
|
Please continue the task from where we left off
|
|
1744
1757
|
`,
|
|
1745
1758
|
},
|
|
1746
1759
|
] as Message[];
|
|
1747
|
-
const
|
|
1760
|
+
const systemMessages = toCompress.filter((m) => m.role === "system");
|
|
1748
1761
|
|
|
1749
1762
|
const newMessages = [
|
|
1750
|
-
...
|
|
1763
|
+
...systemMessages,
|
|
1751
1764
|
...startMessages,
|
|
1752
|
-
...
|
|
1765
|
+
...resumeMessages,
|
|
1753
1766
|
];
|
|
1754
1767
|
|
|
1755
1768
|
const oldLength = this.getMessagesLength(messages);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { exec, spawn, ExecException } from "child_process";
|
|
2
|
-
import { promisify } from "util";
|
|
3
1
|
import * as fs from "fs";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
6
4
|
import { services, ToolsService } from "../../services";
|
|
5
|
+
import { getProcessesDir, ManagedProcess, processDirectory, spawnManaged } from "../../processes/ProcessManager";
|
|
7
6
|
|
|
8
7
|
export const execAsync = promisify(exec);
|
|
9
8
|
|
|
10
9
|
export interface ExecCommandOptions {
|
|
11
|
-
timeout?: number;
|
|
12
|
-
continueInBackground?: boolean;
|
|
13
|
-
maxBuffer?: number;
|
|
14
|
-
logFileName?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
continueInBackground?: boolean;
|
|
12
|
+
maxBuffer?: number;
|
|
13
|
+
logFileName?: string;
|
|
14
|
+
parentProcessId?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
type ExecResult = {
|
|
@@ -20,361 +20,143 @@ type ExecResult = {
|
|
|
20
20
|
timedOut: boolean;
|
|
21
21
|
killed: boolean;
|
|
22
22
|
pid?: number;
|
|
23
|
-
|
|
23
|
+
processId: string;
|
|
24
|
+
processDirectory: string;
|
|
25
|
+
logPath: string;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const STARTED_PIDS = new Set<number>();
|
|
30
|
-
setupProcessCleanup();
|
|
31
|
-
|
|
32
|
-
// ---------- utils ----------
|
|
33
|
-
function stripTrailingAmp(cmd: string) {
|
|
34
|
-
const t = cmd.trim();
|
|
35
|
-
return t.endsWith("&") ? t.replace(/&\s*$/, "").trim() : t;
|
|
28
|
+
function stripTrailingAmp(command: string): string {
|
|
29
|
+
const trimmed = command.trim();
|
|
30
|
+
return trimmed.endsWith("&") ? trimmed.replace(/&\s*$/, "").trim() : trimmed;
|
|
36
31
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Use custom filename if provided, otherwise derive from command
|
|
46
|
-
let baseName = customFileName
|
|
47
|
-
? customFileName.replace(/[^\w.-]+/g, "_")
|
|
48
|
-
: commandNameFrom(cmd);
|
|
49
|
-
|
|
50
|
-
let logPath = path.join(PROCESSES_DIR, `${baseName}.txt`);
|
|
51
|
-
|
|
52
|
-
// If file already exists, append epoch seconds to ensure uniqueness
|
|
53
|
-
if (fs.existsSync(logPath)) {
|
|
54
|
-
const epochSeconds = Math.floor(Date.now() / 1000);
|
|
55
|
-
baseName = `${baseName}_${epochSeconds}`;
|
|
56
|
-
logPath = path.join(PROCESSES_DIR, `${baseName}.txt`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return logPath;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function setupProcessCleanup() {
|
|
63
|
-
const killAll = () => {
|
|
64
|
-
for (const pid of STARTED_PIDS) {
|
|
65
|
-
try {
|
|
66
|
-
if (os.platform() === "win32") {
|
|
67
|
-
spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
68
|
-
stdio: "ignore",
|
|
69
|
-
detached: true,
|
|
70
|
-
}).unref();
|
|
71
|
-
} else {
|
|
72
|
-
try {
|
|
73
|
-
process.kill(-pid, "SIGTERM");
|
|
74
|
-
} catch {
|
|
75
|
-
try {
|
|
76
|
-
process.kill(pid, "SIGTERM");
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch {}
|
|
81
|
-
}
|
|
32
|
+
function readOutput(managed: ManagedProcess, maxBuffer: number): { stdout: string; stderr: string } {
|
|
33
|
+
const read = (file: string) => {
|
|
34
|
+
try {
|
|
35
|
+
const value = fs.readFileSync(file, "utf8");
|
|
36
|
+
return value.length > maxBuffer
|
|
37
|
+
? `${value.slice(0, maxBuffer)}\n[output truncated after ${maxBuffer} characters]`
|
|
38
|
+
: value;
|
|
39
|
+
} catch { return ""; }
|
|
82
40
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
process.exit(143);
|
|
91
|
-
});
|
|
92
|
-
process.once("uncaughtException", (e) => {
|
|
93
|
-
console.error(e);
|
|
94
|
-
killAll();
|
|
95
|
-
process.exit(1);
|
|
96
|
-
});
|
|
97
|
-
process.once("unhandledRejection", (e: any) => {
|
|
98
|
-
console.error(e);
|
|
99
|
-
killAll();
|
|
100
|
-
process.exit(1);
|
|
101
|
-
});
|
|
41
|
+
return { stdout: read(managed.stdoutPath), stderr: read(managed.stderrPath) };
|
|
42
|
+
}
|
|
43
|
+
async function stopAfterTimeout(managed: ManagedProcess): Promise<void> {
|
|
44
|
+
try { managed.signal("SIGTERM"); } catch {}
|
|
45
|
+
try { await managed.wait(3000); return; } catch {}
|
|
46
|
+
try { managed.signal("SIGKILL"); } catch {}
|
|
47
|
+
try { await managed.wait(1000); } catch {}
|
|
102
48
|
}
|
|
103
49
|
|
|
104
|
-
// ---------- core ----------
|
|
105
50
|
const execWithTimeout = async (
|
|
106
51
|
command: string,
|
|
107
|
-
|
|
52
|
+
options: ExecCommandOptions = {}
|
|
108
53
|
): Promise<ExecResult> => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Foreground, indefinite wait → stream with spawn
|
|
119
|
-
if (timeout === -1 && !shouldBg) {
|
|
120
|
-
return new Promise<ExecResult>((resolve) => {
|
|
121
|
-
let out = "";
|
|
122
|
-
let err = "";
|
|
123
|
-
const child = spawn(cleaned, { shell: true });
|
|
124
|
-
child.stdout?.on("data", (d) => (out += d.toString()));
|
|
125
|
-
child.stderr?.on("data", (d) => (err += d.toString()));
|
|
126
|
-
child.once("error", (e) =>
|
|
127
|
-
resolve({
|
|
128
|
-
stdout: out,
|
|
129
|
-
stderr: err || String(e),
|
|
130
|
-
timedOut: false,
|
|
131
|
-
killed: false,
|
|
132
|
-
pid: child.pid ?? undefined,
|
|
133
|
-
})
|
|
134
|
-
);
|
|
135
|
-
child.once("exit", () =>
|
|
136
|
-
resolve({
|
|
137
|
-
stdout: out,
|
|
138
|
-
stderr: err,
|
|
139
|
-
timedOut: false,
|
|
140
|
-
killed: false,
|
|
141
|
-
pid: child.pid ?? undefined,
|
|
142
|
-
})
|
|
143
|
-
);
|
|
144
|
-
});
|
|
54
|
+
const timeout = options.timeout ?? 5000;
|
|
55
|
+
const maxBuffer = options.maxBuffer ?? 16 * 1024 * 1024;
|
|
56
|
+
const background = options.continueInBackground || command.trim().endsWith("&");
|
|
57
|
+
let requestedId = options.logFileName?.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
58
|
+
if (requestedId) {
|
|
59
|
+
const base = requestedId;
|
|
60
|
+
let suffix = 0;
|
|
61
|
+
while (fs.existsSync(processDirectory(requestedId, getProcessesDir()))) requestedId = `${base}-${Date.now()}-${++suffix}`;
|
|
145
62
|
}
|
|
63
|
+
const managed = await spawnManaged(stripTrailingAmp(command), [], {
|
|
64
|
+
id: requestedId,
|
|
65
|
+
shell: true,
|
|
66
|
+
background,
|
|
67
|
+
parentPid: process.pid,
|
|
68
|
+
parentProcessId: options.parentProcessId,
|
|
69
|
+
});
|
|
70
|
+
const initial = managed.status;
|
|
71
|
+
const common = {
|
|
72
|
+
pid: initial.pid || undefined,
|
|
73
|
+
processId: managed.id,
|
|
74
|
+
processDirectory: managed.directory,
|
|
75
|
+
logPath: managed.stdoutPath,
|
|
76
|
+
};
|
|
146
77
|
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
const logPath = makeLogPath(cleaned, opts.logFileName);
|
|
150
|
-
|
|
151
|
-
// Open the log file now; we'll pass this FD to the child so it writes directly.
|
|
152
|
-
// Use 'w' to truncate old logs and guarantee our header goes first.
|
|
153
|
-
const fd = fs.openSync(logPath, "w");
|
|
154
|
-
|
|
155
|
-
// Spawn detached; bind stdout/stderr to the same file.
|
|
156
|
-
const child = spawn(cleaned, {
|
|
157
|
-
shell: true,
|
|
158
|
-
detached: true,
|
|
159
|
-
stdio: ["ignore", fd, fd], // child writes directly to file for its entire lifetime
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Immediately write header (first line includes PID), then close *our* fd.
|
|
163
|
-
// The child keeps its own duplicated handle open.
|
|
164
|
-
const pid = child.pid!;
|
|
165
|
-
const header =
|
|
166
|
-
`PID: ${pid}\n` +
|
|
167
|
-
`CMD: ${cleaned}\n` +
|
|
168
|
-
`START: ${new Date().toISOString()}\n` +
|
|
169
|
-
`---\n`;
|
|
170
|
-
fs.writeSync(fd, header);
|
|
171
|
-
fs.fsyncSync(fd); // flush header before we let go
|
|
172
|
-
try {
|
|
173
|
-
fs.closeSync(fd);
|
|
174
|
-
} catch {}
|
|
175
|
-
|
|
176
|
-
// We only wait 'timeout' ms to return; process keeps running/logging after that.
|
|
177
|
-
return await new Promise<ExecResult>((resolve) => {
|
|
178
|
-
let settled = false;
|
|
179
|
-
|
|
180
|
-
const done = (res: ExecResult) => {
|
|
181
|
-
if (settled) return;
|
|
182
|
-
settled = true;
|
|
183
|
-
resolve({ ...res, pid, logPath });
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
child.once("error", (e) =>
|
|
187
|
-
done({
|
|
188
|
-
stdout: "",
|
|
189
|
-
stderr: `Failed to start command: ${String(e)}`,
|
|
190
|
-
timedOut: false,
|
|
191
|
-
killed: false,
|
|
192
|
-
})
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const tid = setTimeout(() => {
|
|
196
|
-
// fully detach from our side
|
|
197
|
-
try {
|
|
198
|
-
child.unref();
|
|
199
|
-
} catch {}
|
|
200
|
-
STARTED_PIDS.add(pid);
|
|
201
|
-
done({
|
|
202
|
-
stdout: "",
|
|
203
|
-
stderr:
|
|
204
|
-
`Command timed out after ${timeout}ms but continues in background\n` +
|
|
205
|
-
`Logs: ${logPath}\n` +
|
|
206
|
-
`Tip: read first line for PID; kill by PID for cleanup.\n`,
|
|
207
|
-
timedOut: true,
|
|
208
|
-
killed: false,
|
|
209
|
-
});
|
|
210
|
-
}, timeout);
|
|
211
|
-
|
|
212
|
-
// If it finishes early, report success and avoid “timed out” messaging
|
|
213
|
-
child.once("exit", () => {
|
|
214
|
-
clearTimeout(tid);
|
|
215
|
-
done({
|
|
216
|
-
stdout: "",
|
|
217
|
-
stderr: `Process finished before timeout. Logs: ${logPath}\n`,
|
|
218
|
-
timedOut: false,
|
|
219
|
-
killed: false,
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
});
|
|
78
|
+
if (background) {
|
|
79
|
+
return { stdout: "", stderr: "", timedOut: false, killed: false, ...common };
|
|
223
80
|
}
|
|
224
81
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
stdout: out,
|
|
238
|
-
stderr: err || error.message,
|
|
239
|
-
timedOut: (error as any).killed ?? false,
|
|
240
|
-
killed: !!(error as any).killed,
|
|
241
|
-
pid: (child as any).pid ?? undefined,
|
|
242
|
-
});
|
|
243
|
-
} else {
|
|
244
|
-
resolve({
|
|
245
|
-
stdout: out,
|
|
246
|
-
stderr: err,
|
|
247
|
-
timedOut: false,
|
|
248
|
-
killed: false,
|
|
249
|
-
pid: (child as any).pid ?? undefined,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
);
|
|
254
|
-
child.stdout?.on("data", (d) => (out += d.toString()));
|
|
255
|
-
child.stderr?.on("data", (d) => (err += d.toString()));
|
|
256
|
-
});
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
if (timeout === -1) {
|
|
84
|
+
await managed.wait();
|
|
85
|
+
} else {
|
|
86
|
+
try { await managed.wait(timeout); }
|
|
87
|
+
catch {
|
|
88
|
+
timedOut = true;
|
|
89
|
+
await stopAfterTimeout(managed);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const output = readOutput(managed, maxBuffer);
|
|
93
|
+
return { ...output, timedOut, killed: timedOut, ...common };
|
|
257
94
|
};
|
|
258
95
|
|
|
259
|
-
|
|
96
|
+
/** Execute a shell command through the durable managed-process subsystem. */
|
|
260
97
|
export async function execCommand(
|
|
261
98
|
command: string,
|
|
262
99
|
timeout?: number | string,
|
|
263
100
|
continueInBackground?: boolean,
|
|
264
|
-
logFileName?: string
|
|
101
|
+
logFileName?: string,
|
|
102
|
+
_ctx?: { taskId?: string }
|
|
265
103
|
): Promise<string> {
|
|
266
104
|
if (!command || typeof command !== "string") {
|
|
267
105
|
throw new Error("Invalid command. We received a non-string value. Please ensure you are sending strings of 4k tokens or less.");
|
|
268
106
|
}
|
|
269
107
|
|
|
270
|
-
|
|
271
|
-
const toolService = (
|
|
272
|
-
this instanceof ToolsService ? this : services().Tools
|
|
273
|
-
) as ToolsService;
|
|
274
|
-
|
|
275
|
-
// Coerce timeout to a number. Agents (and some tool-call serializers) often
|
|
276
|
-
// pass numeric arguments as strings (e.g. "120000"). Node's exec()/setTimeout
|
|
277
|
-
// reject non-integer values with "The value of \"timeout\" is out of range.
|
|
278
|
-
// It must be an unsigned integer", so normalize here before any use.
|
|
108
|
+
const toolService = (this instanceof ToolsService ? this : services().Tools) as ToolsService;
|
|
279
109
|
let normalizedTimeout: number | undefined;
|
|
280
110
|
const rawTimeout = typeof timeout === "string" ? Number(timeout.trim()) : timeout;
|
|
281
|
-
if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// -1 is the sentinel for "wait indefinitely" — leave as-is.
|
|
285
|
-
normalizedTimeout = -1;
|
|
286
|
-
} else {
|
|
287
|
-
// Clamp to a non-negative integer so the underlying APIs never throw.
|
|
288
|
-
normalizedTimeout = Math.max(0, Math.floor(rawTimeout));
|
|
289
|
-
}
|
|
111
|
+
if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout)) normalizedTimeout = undefined;
|
|
112
|
+
else if (rawTimeout === -1) normalizedTimeout = -1;
|
|
113
|
+
else normalizedTimeout = Math.max(0, Math.floor(rawTimeout));
|
|
290
114
|
|
|
291
|
-
// Detect suspiciously small timeout values — agents often pass seconds instead of ms.
|
|
292
|
-
// Any value < 100 is almost certainly a mistake (100ms is still very fast for a shell cmd).
|
|
293
|
-
// We auto-correct to seconds and include a warning in the output so the agent learns.
|
|
294
115
|
let correctedTimeout = normalizedTimeout;
|
|
295
116
|
let timeoutWarning = "";
|
|
296
|
-
if (
|
|
297
|
-
normalizedTimeout !== undefined &&
|
|
298
|
-
normalizedTimeout !== -1 &&
|
|
299
|
-
normalizedTimeout < 100
|
|
300
|
-
) {
|
|
117
|
+
if (normalizedTimeout !== undefined && normalizedTimeout !== -1 && normalizedTimeout < 100) {
|
|
301
118
|
correctedTimeout = normalizedTimeout * 1000;
|
|
302
|
-
timeoutWarning =
|
|
303
|
-
`⚠️ Warning: timeout was ${normalizedTimeout}ms which is extremely small and likely a mistake. ` +
|
|
304
|
-
`The timeout unit is milliseconds, not seconds. ` +
|
|
305
|
-
`Auto-corrected to ${correctedTimeout}ms (${normalizedTimeout}s).\n\n`;
|
|
119
|
+
timeoutWarning = `⚠️ Warning: timeout was ${normalizedTimeout}ms which is extremely small and likely a mistake. The timeout unit is milliseconds, not seconds. Auto-corrected to ${correctedTimeout}ms (${normalizedTimeout}s).\n\n`;
|
|
306
120
|
}
|
|
307
121
|
|
|
308
122
|
const context = toolService.getContext();
|
|
309
|
-
|
|
310
|
-
// Emit pre-run blocking event — handlers can throw to block the command
|
|
311
123
|
if (context.Events) {
|
|
312
|
-
await context.Events.emitBlocking("exec:pre-run", {
|
|
313
|
-
command,
|
|
314
|
-
timeout,
|
|
315
|
-
continueInBackground,
|
|
316
|
-
logFileName,
|
|
317
|
-
});
|
|
124
|
+
await context.Events.emitBlocking("exec:pre-run", { command, timeout, continueInBackground, logFileName });
|
|
318
125
|
}
|
|
319
126
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
127
|
+
|
|
128
|
+
const parentProcessId = process.env.KNOWHOW_MANAGED_PROCESS_ID || _ctx?.taskId;
|
|
129
|
+
const result = await execWithTimeout(command, {
|
|
130
|
+
timeout: correctedTimeout,
|
|
131
|
+
continueInBackground,
|
|
132
|
+
logFileName,
|
|
133
|
+
parentProcessId,
|
|
134
|
+
});
|
|
135
|
+
const { stdout, stderr, timedOut, killed, pid, processId, processDirectory, logPath } = result;
|
|
326
136
|
|
|
327
137
|
let output = "";
|
|
328
|
-
if (stderr) output += stderr + "\n";
|
|
138
|
+
if (stderr) output += stderr + (stderr.endsWith("\n") ? "" : "\n");
|
|
329
139
|
if (stdout) output += stdout;
|
|
330
|
-
|
|
331
|
-
const statusMsg =
|
|
332
|
-
?
|
|
140
|
+
const background = continueInBackground || command.trim().endsWith("&");
|
|
141
|
+
const statusMsg = background
|
|
142
|
+
? ` (managed background process, id=${processId}, pid=${pid || "starting"})`
|
|
143
|
+
: timedOut
|
|
333
144
|
? " (killed due to timeout)"
|
|
334
|
-
:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
/*
|
|
340
|
-
*const lines = output.split("\n");
|
|
341
|
-
*const maxLines = 1000;
|
|
342
|
-
*const maxChars = 40000;
|
|
343
|
-
*const trimmed = (lines.length > maxLines ? lines.slice(0, maxLines) : lines)
|
|
344
|
-
* .join("\n")
|
|
345
|
-
* .slice(0, maxChars);
|
|
346
|
-
*const trimmedMsg =
|
|
347
|
-
* lines.length > maxLines
|
|
348
|
-
* ? ` (${lines.length - maxLines} results trimmed)`
|
|
349
|
-
* : "";
|
|
350
|
-
*/
|
|
351
|
-
|
|
352
|
-
const result = `$ ${command}${statusMsg}\n${output}`;
|
|
145
|
+
: "";
|
|
146
|
+
if (background) {
|
|
147
|
+
output += `Process directory: ${processDirectory}\nStdout: ${logPath}\nStderr: ${result.processDirectory}/stderr\n`;
|
|
148
|
+
}
|
|
149
|
+
const rendered = `$ ${command}${statusMsg}\n${output}`;
|
|
353
150
|
|
|
354
|
-
// Emit post-run blocking event — handlers can append extra context
|
|
355
151
|
let eventResults: any[] = [];
|
|
356
152
|
if (context.Events) {
|
|
357
153
|
eventResults = await context.Events.emitBlocking("exec:post-run", {
|
|
358
|
-
command,
|
|
359
|
-
|
|
360
|
-
continueInBackground,
|
|
361
|
-
logFileName,
|
|
362
|
-
stdout,
|
|
363
|
-
stderr,
|
|
364
|
-
timedOut,
|
|
365
|
-
killed,
|
|
366
|
-
pid,
|
|
367
|
-
logPath,
|
|
368
|
-
output,
|
|
154
|
+
command, timeout, continueInBackground, logFileName,
|
|
155
|
+
stdout, stderr, timedOut, killed, pid, logPath, processId, processDirectory, output,
|
|
369
156
|
});
|
|
370
157
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
"\n\nAdditional Information:\n" + JSON.stringify(eventResults, null, 2);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return timeoutWarning + result + eventResultsText;
|
|
380
|
-
};
|
|
158
|
+
const eventResultsText = eventResults?.length
|
|
159
|
+
? "\n\nAdditional Information:\n" + JSON.stringify(eventResults, null, 2)
|
|
160
|
+
: "";
|
|
161
|
+
return timeoutWarning + rendered + eventResultsText;
|
|
162
|
+
}
|
package/src/agents/tools/list.ts
CHANGED
|
@@ -88,7 +88,7 @@ export const includedTools = [
|
|
|
88
88
|
function: {
|
|
89
89
|
name: "execCommand",
|
|
90
90
|
description:
|
|
91
|
-
"Execute a command
|
|
91
|
+
"Execute a command as a durable managed process. Use this to run tests and terminal tasks. Use timeout: -1 to wait indefinitely. Commands ending with '&' or with continueInBackground=true survive the calling agent and return a process ID plus a .knowhow/processes/<process-id>/ directory containing status, stdin, and output logs. Use `knowhow processes` to list, inspect, send input, attach, or stop them.",
|
|
92
92
|
parameters: {
|
|
93
93
|
type: "object",
|
|
94
94
|
positional: true,
|
|
@@ -110,7 +110,7 @@ export const includedTools = [
|
|
|
110
110
|
logFileName: {
|
|
111
111
|
type: "string",
|
|
112
112
|
description:
|
|
113
|
-
"Optional
|
|
113
|
+
"Optional preferred managed process ID (sanitized for use as a directory name). If it already exists, a timestamp suffix is added.",
|
|
114
114
|
},
|
|
115
115
|
},
|
|
116
116
|
required: ["command"],
|