@tyvm/knowhow 0.0.147 → 0.0.148
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 +27 -23
- package/src/agents/tools/execCommand.ts +102 -320
- package/src/agents/tools/list.ts +2 -2
- package/src/agents/tools/startAgentTask.ts +50 -25
- package/src/ai.ts +4 -1
- package/src/chat/CliChatService.ts +23 -2
- 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 +2 -0
- package/src/commands/agents.ts +53 -8
- package/src/commands/processes.ts +138 -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 +115 -0
- 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/index.ts +6 -0
- package/src/services/modules/types.ts +4 -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 +16 -12
- 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 +35 -19
- 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/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/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 +4 -0
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -0
- package/ts_build/src/commands/agents.js +42 -7
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/processes.d.ts +2 -0
- package/ts_build/src/commands/processes.js +213 -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 +161 -0
- package/ts_build/src/processes/managedProcessShim.js.map +1 -0
- 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/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/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
|
|
|
@@ -1689,15 +1686,24 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1689
1686
|
return this.taskBreakdown;
|
|
1690
1687
|
}
|
|
1691
1688
|
|
|
1692
|
-
async compressMessages(
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1689
|
+
async compressMessages(messages: Message[]) {
|
|
1690
|
+
// Preserve the latest agent interaction exactly. Starting the resumed thread
|
|
1691
|
+
// at the final assistant message keeps its tool calls, every tool response,
|
|
1692
|
+
// and any subsequent user messages together as one protocol-valid unit.
|
|
1693
|
+
let resumeIndex = messages.length - 1;
|
|
1694
|
+
while (
|
|
1695
|
+
resumeIndex >= 0 &&
|
|
1696
|
+
messages[resumeIndex].role !== "assistant"
|
|
1697
|
+
) {
|
|
1698
|
+
resumeIndex--;
|
|
1699
|
+
}
|
|
1700
|
+
const compressionEnd = resumeIndex === -1 ? messages.length : resumeIndex;
|
|
1701
|
+
const toCompress = messages.slice(0, compressionEnd);
|
|
1702
|
+
const resumeMessages = messages.slice(compressionEnd);
|
|
1703
|
+
|
|
1697
1704
|
this.log(
|
|
1698
|
-
`Compressing messages from ${
|
|
1705
|
+
`Compressing messages from 0 to ${compressionEnd}, resuming from ${resumeIndex}, total messages: ${messages.length}`
|
|
1699
1706
|
);
|
|
1700
|
-
const toCompress = messages.slice(startIndex, endIndex);
|
|
1701
1707
|
const toCompressPrompt = `We are compressing our conversation to save memory.
|
|
1702
1708
|
Please summarize the conversation so far, so that we may continue the original task with a smaller context
|
|
1703
1709
|
|
|
@@ -1709,14 +1715,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1709
1715
|
|
|
1710
1716
|
Our initial task breakdown: ${this.taskBreakdown}
|
|
1711
1717
|
|
|
1712
|
-
This summary will
|
|
1718
|
+
This summary will replace the older history. The latest agent interaction will remain verbatim:
|
|
1713
1719
|
|
|
1714
1720
|
`;
|
|
1715
1721
|
|
|
1716
|
-
const model = this.getModel();
|
|
1717
|
-
|
|
1718
1722
|
const compressMessagesPayload = [
|
|
1719
|
-
...
|
|
1723
|
+
...toCompress,
|
|
1720
1724
|
{
|
|
1721
1725
|
role: "user",
|
|
1722
1726
|
content: toCompressPrompt,
|
|
@@ -1744,12 +1748,12 @@ export abstract class BaseAgent implements IAgent {
|
|
|
1744
1748
|
`,
|
|
1745
1749
|
},
|
|
1746
1750
|
] as Message[];
|
|
1747
|
-
const
|
|
1751
|
+
const systemMessages = toCompress.filter((m) => m.role === "system");
|
|
1748
1752
|
|
|
1749
1753
|
const newMessages = [
|
|
1750
|
-
...
|
|
1754
|
+
...systemMessages,
|
|
1751
1755
|
...startMessages,
|
|
1752
|
-
...
|
|
1756
|
+
...resumeMessages,
|
|
1753
1757
|
];
|
|
1754
1758
|
|
|
1755
1759
|
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"],
|