@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
|
|
6
|
+
export type ManagedProcessState = "starting" | "running" | "exited" | "failed" | "stopped";
|
|
7
|
+
export interface ManagedProcessStatus {
|
|
8
|
+
id: string; pid: number | null; shimPid: number | null; state: ManagedProcessState;
|
|
9
|
+
command: string; args: string[]; cwd: string; parentProcessId: string | null;
|
|
10
|
+
parentPid: number | null; background: boolean; startedAt: string; endedAt: string | null;
|
|
11
|
+
exitCode: number | null; signal: string | null; error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SpawnManagedOptions {
|
|
14
|
+
id?: string; cwd?: string; env?: NodeJS.ProcessEnv; background?: boolean;
|
|
15
|
+
parentProcessId?: string; parentPid?: number; shell?: boolean; processesDir?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ManagedProcessInfo { id: string; directory: string; status: ManagedProcessStatus; }
|
|
18
|
+
|
|
19
|
+
export function getProcessesDir(cwd = process.cwd()): string {
|
|
20
|
+
return process.env.KNOWHOW_PROCESSES_DIR || path.join(cwd, ".knowhow", "processes");
|
|
21
|
+
}
|
|
22
|
+
export function processDirectory(id: string, processesDir = getProcessesDir()): string {
|
|
23
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error(`Invalid process id: ${id}`);
|
|
24
|
+
return path.join(processesDir, id);
|
|
25
|
+
}
|
|
26
|
+
export function readManagedStatus(id: string, processesDir = getProcessesDir()): ManagedProcessStatus {
|
|
27
|
+
return JSON.parse(fs.readFileSync(path.join(processDirectory(id, processesDir), "status.json"), "utf8"));
|
|
28
|
+
}
|
|
29
|
+
export function listManagedProcesses(processesDir = getProcessesDir()): ManagedProcessInfo[] {
|
|
30
|
+
if (!fs.existsSync(processesDir)) return [];
|
|
31
|
+
return fs.readdirSync(processesDir, { withFileTypes: true })
|
|
32
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(processesDir, entry.name, "status.json")))
|
|
33
|
+
.flatMap((entry) => { try { return [{ id: entry.name, directory: path.join(processesDir, entry.name), status: readManagedStatus(entry.name, processesDir) }]; } catch { return []; } })
|
|
34
|
+
.sort((a, b) => b.status.startedAt.localeCompare(a.status.startedAt));
|
|
35
|
+
}
|
|
36
|
+
function makeId(command: string): string {
|
|
37
|
+
const name = path.basename(command).replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 28) || "process";
|
|
38
|
+
return `${name}-${Date.now()}-${randomBytes(3).toString("hex")}`;
|
|
39
|
+
}
|
|
40
|
+
function waitForFile(file: string, timeoutMs: number): Promise<void> {
|
|
41
|
+
const started = Date.now();
|
|
42
|
+
return new Promise((resolve, reject) => { const check = () => {
|
|
43
|
+
if (fs.existsSync(file)) return resolve();
|
|
44
|
+
if (Date.now() - started >= timeoutMs) return reject(new Error(`Managed process did not initialize: ${file}`));
|
|
45
|
+
setTimeout(check, 20);
|
|
46
|
+
}; check(); });
|
|
47
|
+
}
|
|
48
|
+
export class ManagedProcess {
|
|
49
|
+
constructor(public readonly id: string, public readonly directory: string) {}
|
|
50
|
+
get status(): ManagedProcessStatus { return readManagedStatus(this.id, path.dirname(this.directory)); }
|
|
51
|
+
get stdoutPath(): string { return path.join(this.directory, "stdout"); }
|
|
52
|
+
get stderrPath(): string { return path.join(this.directory, "stderr"); }
|
|
53
|
+
get inputLogPath(): string { return path.join(this.directory, "input.log"); }
|
|
54
|
+
get stdinPath(): string { return path.join(this.directory, "stdin"); }
|
|
55
|
+
async write(input: string): Promise<void> {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
await fs.promises.appendFile(this.stdinPath, input);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// A non-blocking open fails promptly with ENXIO if the shim is gone instead
|
|
61
|
+
// of leaving callers hung forever waiting for a FIFO reader.
|
|
62
|
+
const handle = await fs.promises.open(this.stdinPath, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
|
63
|
+
try { await handle.writeFile(input); } finally { await handle.close(); }
|
|
64
|
+
}
|
|
65
|
+
signal(signal: NodeJS.Signals = "SIGTERM"): void {
|
|
66
|
+
const status = this.status;
|
|
67
|
+
if (!status.pid) throw new Error(`Process ${this.id} has no active pid`);
|
|
68
|
+
try { process.kill(-status.pid, signal); } catch { process.kill(status.pid, signal); }
|
|
69
|
+
}
|
|
70
|
+
async wait(timeoutMs?: number): Promise<ManagedProcessStatus> {
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
while (true) {
|
|
73
|
+
const status = this.status;
|
|
74
|
+
if (!["starting", "running"].includes(status.state)) return status;
|
|
75
|
+
if (timeoutMs !== undefined && Date.now() - started >= timeoutMs) throw new Error(`Timed out waiting for managed process ${this.id} after ${timeoutMs}ms`);
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function spawnManaged(command: string, args: string[] = [], options: SpawnManagedOptions = {}): Promise<ManagedProcess> {
|
|
81
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
82
|
+
const processesDir = options.processesDir || getProcessesDir(cwd);
|
|
83
|
+
const id = options.id || makeId(command);
|
|
84
|
+
const directory = processDirectory(id, processesDir);
|
|
85
|
+
fs.mkdirSync(processesDir, { recursive: true });
|
|
86
|
+
fs.mkdirSync(directory);
|
|
87
|
+
const configPath = path.join(directory, "launch.json");
|
|
88
|
+
fs.writeFileSync(configPath, JSON.stringify({ id, command, args, cwd, env: options.env || {}, background: options.background === true, parentProcessId: options.parentProcessId || null, parentPid: options.parentPid ?? process.pid, shell: options.shell === true, processesDir }, null, 2));
|
|
89
|
+
const cliPath = path.resolve(__dirname, "..", "cli.js");
|
|
90
|
+
const shim = spawn(process.execPath, [cliPath, "processes", "run", "--config", configPath], { cwd, detached: true, stdio: "ignore", env: process.env });
|
|
91
|
+
shim.unref();
|
|
92
|
+
try { await waitForFile(path.join(directory, "status.json"), 5000); }
|
|
93
|
+
catch (error) { try { fs.rmSync(directory, { recursive: true, force: true }); } catch {} throw error; }
|
|
94
|
+
return new ManagedProcess(id, directory);
|
|
95
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn, spawnSync, ChildProcess } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ManagedProcessStatus } from "./ProcessManager";
|
|
5
|
+
|
|
6
|
+
interface LaunchConfig {
|
|
7
|
+
id: string; command: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv;
|
|
8
|
+
background: boolean; parentProcessId: string | null; parentPid: number | null;
|
|
9
|
+
shell: boolean; processesDir: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function atomicJson(file: string, value: unknown): void {
|
|
13
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2));
|
|
15
|
+
fs.renameSync(tmp, file);
|
|
16
|
+
}
|
|
17
|
+
function alive(pid: number): boolean {
|
|
18
|
+
try { process.kill(pid, 0); return true; } catch (error: any) { return error?.code === "EPERM"; }
|
|
19
|
+
}
|
|
20
|
+
function terminateGroup(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
21
|
+
if (!child.pid) return;
|
|
22
|
+
try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch {} }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runManagedProcessShim(configPath: string): Promise<void> {
|
|
26
|
+
const config: LaunchConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
27
|
+
const dir = path.dirname(configPath);
|
|
28
|
+
const startedAt = new Date().toISOString();
|
|
29
|
+
const statusPath = path.join(dir, "status.json");
|
|
30
|
+
const stdoutPath = path.join(dir, "stdout");
|
|
31
|
+
const stderrPath = path.join(dir, "stderr");
|
|
32
|
+
const inputLogPath = path.join(dir, "input.log");
|
|
33
|
+
const fifoPath = path.join(dir, "stdin");
|
|
34
|
+
fs.closeSync(fs.openSync(stdoutPath, "a"));
|
|
35
|
+
fs.closeSync(fs.openSync(stderrPath, "a"));
|
|
36
|
+
fs.closeSync(fs.openSync(inputLogPath, "a"));
|
|
37
|
+
|
|
38
|
+
if (process.platform !== "win32") {
|
|
39
|
+
const result = spawnSync("mkfifo", [fifoPath]);
|
|
40
|
+
if (result.status !== 0 && !fs.existsSync(fifoPath)) throw new Error(`Unable to create stdin FIFO: ${result.stderr?.toString()}`);
|
|
41
|
+
} else {
|
|
42
|
+
// Windows has no POSIX FIFO. Keep the same path as an append-only inbox fallback.
|
|
43
|
+
fs.closeSync(fs.openSync(fifoPath, "a"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let status: ManagedProcessStatus = {
|
|
47
|
+
id: config.id, pid: null, shimPid: process.pid, state: "starting", command: config.command,
|
|
48
|
+
args: config.args, cwd: config.cwd, parentProcessId: config.parentProcessId,
|
|
49
|
+
parentPid: config.parentPid, background: config.background, startedAt, endedAt: null,
|
|
50
|
+
exitCode: null, signal: null,
|
|
51
|
+
};
|
|
52
|
+
const writeStatus = () => atomicJson(statusPath, status);
|
|
53
|
+
atomicJson(path.join(dir, "metadata.json"), {
|
|
54
|
+
id: config.id, command: config.command, args: config.args, cwd: config.cwd,
|
|
55
|
+
parentProcessId: config.parentProcessId, parentPid: config.parentPid,
|
|
56
|
+
background: config.background, createdAt: startedAt, shimPid: process.pid,
|
|
57
|
+
});
|
|
58
|
+
fs.writeFileSync(path.join(dir, "pid"), "");
|
|
59
|
+
writeStatus();
|
|
60
|
+
|
|
61
|
+
const stdoutFd = fs.openSync(stdoutPath, "a");
|
|
62
|
+
const stderrFd = fs.openSync(stderrPath, "a");
|
|
63
|
+
const child = spawn(config.command, config.args, {
|
|
64
|
+
cwd: config.cwd, shell: config.shell, detached: process.platform !== "win32",
|
|
65
|
+
stdio: ["pipe", stdoutFd, stderrFd],
|
|
66
|
+
env: { ...process.env, ...config.env, KNOWHOW_MANAGED_PROCESS_ID: config.id, KNOWHOW_MANAGED_PROCESS_DIR: dir },
|
|
67
|
+
});
|
|
68
|
+
fs.closeSync(stdoutFd); fs.closeSync(stderrFd);
|
|
69
|
+
status = { ...status, pid: child.pid || null, state: "running" };
|
|
70
|
+
fs.writeFileSync(path.join(dir, "pid"), child.pid ? `${child.pid}\n` : "");
|
|
71
|
+
writeStatus();
|
|
72
|
+
|
|
73
|
+
let fifoStream: fs.ReadStream | undefined;
|
|
74
|
+
if (process.platform !== "win32") {
|
|
75
|
+
// O_RDWR prevents transient writers disconnecting from delivering EOF to the shim.
|
|
76
|
+
const fifoFd = fs.openSync(fifoPath, fs.constants.O_RDWR);
|
|
77
|
+
fifoStream = fs.createReadStream("", { fd: fifoFd, autoClose: true });
|
|
78
|
+
fifoStream.on("data", (chunk) => {
|
|
79
|
+
fs.appendFileSync(inputLogPath, chunk);
|
|
80
|
+
if (child.stdin?.writable) child.stdin.write(chunk);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let stoppingForParent = false;
|
|
85
|
+
const parentTimer = !config.background && config.parentPid
|
|
86
|
+
? setInterval(() => {
|
|
87
|
+
if (!alive(config.parentPid!)) {
|
|
88
|
+
stoppingForParent = true;
|
|
89
|
+
terminateGroup(child, "SIGTERM");
|
|
90
|
+
setTimeout(() => { if (alive(child.pid || -1)) terminateGroup(child, "SIGKILL"); }, 3000).unref();
|
|
91
|
+
}
|
|
92
|
+
}, 500)
|
|
93
|
+
: undefined;
|
|
94
|
+
parentTimer?.unref();
|
|
95
|
+
|
|
96
|
+
const forward = (signal: NodeJS.Signals) => terminateGroup(child, signal);
|
|
97
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
98
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
99
|
+
process.once("SIGHUP", () => forward("SIGHUP"));
|
|
100
|
+
|
|
101
|
+
await new Promise<void>((resolve) => {
|
|
102
|
+
child.once("error", (error) => {
|
|
103
|
+
status = { ...status, state: "failed", error: error.message, endedAt: new Date().toISOString() };
|
|
104
|
+
writeStatus(); resolve();
|
|
105
|
+
});
|
|
106
|
+
child.once("exit", (code, signal) => {
|
|
107
|
+
status = { ...status, state: stoppingForParent || signal ? "stopped" : code === 0 ? "exited" : "failed", endedAt: new Date().toISOString(), exitCode: code, signal };
|
|
108
|
+
writeStatus(); resolve();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
if (parentTimer) clearInterval(parentTimer);
|
|
112
|
+
if (fifoStream && !fifoStream.closed) {
|
|
113
|
+
// A FIFO read cannot always be cancelled by closing its descriptor (notably
|
|
114
|
+
// on macOS). Wake the pending read before asking ReadStream to close it;
|
|
115
|
+
// otherwise each completed command can leave its shim wedged in close(2).
|
|
116
|
+
const wakeFd = fs.openSync(fifoPath, fs.constants.O_WRONLY | fs.constants.O_NONBLOCK);
|
|
117
|
+
try {
|
|
118
|
+
fs.writeSync(wakeFd, Buffer.from([0]));
|
|
119
|
+
} finally {
|
|
120
|
+
fs.closeSync(wakeFd);
|
|
121
|
+
}
|
|
122
|
+
await new Promise<void>((resolve) => {
|
|
123
|
+
fifoStream!.once("close", resolve);
|
|
124
|
+
fifoStream!.destroy();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -251,6 +251,24 @@ export class Base64ImageProcessor {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
private isUsableImageUrl(url: unknown): url is string {
|
|
255
|
+
if (typeof url !== "string") return false;
|
|
256
|
+
|
|
257
|
+
// Data URLs are only usable when they contain an image format this processor
|
|
258
|
+
// recognizes. In particular, reject TokenCompressor placeholders that happen
|
|
259
|
+
// to be located inside an otherwise valid-looking image_url content part.
|
|
260
|
+
if (url.startsWith("data:")) {
|
|
261
|
+
return this.isBase64Image(url).isImage;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const parsed = new URL(url);
|
|
266
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
254
272
|
private processToolMessageContent(message: Message): void {
|
|
255
273
|
// Tool messages have string content that might be a JSON string containing image data
|
|
256
274
|
if (typeof message.content === "string" && message.content.trim()) {
|
|
@@ -267,13 +285,19 @@ export class Base64ImageProcessor {
|
|
|
267
285
|
// model to see a raw base64 JSON string instead of an actual image.)
|
|
268
286
|
const candidates = Array.isArray(parsed) ? parsed : [parsed];
|
|
269
287
|
const isImagePart = (p: any) =>
|
|
270
|
-
p &&
|
|
288
|
+
p &&
|
|
289
|
+
p.type === "image_url" &&
|
|
290
|
+
this.isUsableImageUrl(p.image_url?.url);
|
|
271
291
|
const isTextPart = (p: any) =>
|
|
272
292
|
p && p.type === "text" && typeof p.text === "string";
|
|
293
|
+
const hasInvalidImagePart = candidates.some(
|
|
294
|
+
(p: any) => p?.type === "image_url" && !isImagePart(p)
|
|
295
|
+
);
|
|
273
296
|
|
|
274
|
-
// Only rewrite
|
|
275
|
-
//
|
|
276
|
-
|
|
297
|
+
// Only rewrite content containing valid provider-ready images. If an
|
|
298
|
+
// image URL was compressed or is malformed, preserve the JSON as text so
|
|
299
|
+
// the agent can inspect/expand it instead of failing the next API call.
|
|
300
|
+
if (!hasInvalidImagePart && candidates.some(isImagePart)) {
|
|
277
301
|
message.content = candidates.filter(
|
|
278
302
|
(p) => isImagePart(p) || isTextPart(p)
|
|
279
303
|
);
|
|
@@ -398,6 +398,20 @@ export class JsonCompressor {
|
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
// Image content parts must remain atomic. Compressing a data URL independently
|
|
402
|
+
// leaves a valid-looking image_url object whose URL is only a placeholder.
|
|
403
|
+
// The containing response can still be compressed, but expanding it must
|
|
404
|
+
// restore an image part with its usable URL intact.
|
|
405
|
+
if (
|
|
406
|
+
obj &&
|
|
407
|
+
typeof obj === "object" &&
|
|
408
|
+
!Array.isArray(obj) &&
|
|
409
|
+
obj.type === "image_url" &&
|
|
410
|
+
typeof obj.image_url?.url === "string"
|
|
411
|
+
) {
|
|
412
|
+
return obj;
|
|
413
|
+
}
|
|
414
|
+
|
|
401
415
|
// Handle objects - try low-signal detection first, then process properties (depth-first)
|
|
402
416
|
if (obj && typeof obj === "object") {
|
|
403
417
|
// Check if this exact object (by original content) is a duplicate
|
|
@@ -45,6 +45,22 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
45
45
|
// JSON compression handler
|
|
46
46
|
private jsonCompressor: JsonCompressor;
|
|
47
47
|
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tool names whose responses should never be compressed.
|
|
51
|
+
* These are compression/retrieval tools themselves - compressing their output
|
|
52
|
+
* would prevent the agent from seeing the uncompressed data it just retrieved.
|
|
53
|
+
*/
|
|
54
|
+
static DO_NOT_COMPRESS: ReadonlySet<string> = new Set([
|
|
55
|
+
"expandTokens",
|
|
56
|
+
"grepToolResponse",
|
|
57
|
+
"jqToolResponse",
|
|
58
|
+
"tailToolResponse",
|
|
59
|
+
"listStoredToolResponses",
|
|
60
|
+
"loadImageAsBase64" // need a proper way to do this
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
|
|
48
64
|
constructor(toolsService?: ToolsService) {
|
|
49
65
|
this.jsonCompressor = new JsonCompressor(
|
|
50
66
|
this,
|
|
@@ -290,19 +306,6 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
290
306
|
}
|
|
291
307
|
}
|
|
292
308
|
|
|
293
|
-
/**
|
|
294
|
-
* Tool names whose responses should never be compressed.
|
|
295
|
-
* These are compression/retrieval tools themselves - compressing their output
|
|
296
|
-
* would prevent the agent from seeing the uncompressed data it just retrieved.
|
|
297
|
-
*/
|
|
298
|
-
static readonly COMPRESSION_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
299
|
-
"expandTokens",
|
|
300
|
-
"grepToolResponse",
|
|
301
|
-
"jqToolResponse",
|
|
302
|
-
"tailToolResponse",
|
|
303
|
-
"listStoredToolResponses",
|
|
304
|
-
]);
|
|
305
|
-
|
|
306
309
|
createProcessor(
|
|
307
310
|
filterFn?: (msg: Message) => boolean
|
|
308
311
|
): MessageProcessorFunction {
|
|
@@ -344,7 +347,7 @@ export class TokenCompressor implements JsonCompressorStorage {
|
|
|
344
347
|
if (
|
|
345
348
|
message.role === "tool" &&
|
|
346
349
|
message.name &&
|
|
347
|
-
TokenCompressor.
|
|
350
|
+
TokenCompressor.DO_NOT_COMPRESS.has(message.name)
|
|
348
351
|
) {
|
|
349
352
|
continue;
|
|
350
353
|
}
|
|
@@ -76,14 +76,14 @@ export class AgentService {
|
|
|
76
76
|
return this.agentContext ?? { Tools: this.tools, Events: this.events };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
public getAgent(name: string):
|
|
79
|
+
public getAgent<T extends IAgent = IAgent>(name: string): T {
|
|
80
80
|
const agent = this.agents.get(name);
|
|
81
81
|
if (!agent) {
|
|
82
82
|
throw new Error(
|
|
83
83
|
`Agent ${name} not found. Options are: ${this.listAgents()}`
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
|
-
return agent;
|
|
86
|
+
return agent as T;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
public listAgents(): string[] {
|
|
@@ -124,7 +124,18 @@ export class AgentSyncFs {
|
|
|
124
124
|
|
|
125
125
|
try {
|
|
126
126
|
const metadataPath = path.join(this.taskPath, "metadata.json");
|
|
127
|
-
|
|
127
|
+
// Thread/status updates are partial snapshots. Merge them so stable
|
|
128
|
+
// orchestration identity written by createTask (taskId, pid and parent)
|
|
129
|
+
// is never discarded by a later update.
|
|
130
|
+
let existing: Record<string, unknown> = {};
|
|
131
|
+
try {
|
|
132
|
+
existing = JSON.parse(await fs.readFile(metadataPath, "utf8"));
|
|
133
|
+
} catch { /* first write or an interrupted legacy write */ }
|
|
134
|
+
await fs.writeFile(
|
|
135
|
+
metadataPath,
|
|
136
|
+
JSON.stringify({ ...existing, ...data }, null, 2),
|
|
137
|
+
"utf8"
|
|
138
|
+
);
|
|
128
139
|
} catch (error) {
|
|
129
140
|
console.error(`❌ Failed to write metadata:`, error);
|
|
130
141
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ModuleExtension {
|
|
2
|
+
type: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface RegisteredExtension extends ModuleExtension {
|
|
7
|
+
owner: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Process-local registry for optional, consumer-defined module capabilities. */
|
|
11
|
+
export class ExtensionsService {
|
|
12
|
+
private extensions: RegisteredExtension[] = [];
|
|
13
|
+
|
|
14
|
+
register(owner: string, extension: ModuleExtension): void {
|
|
15
|
+
this.extensions.push({ ...extension, owner });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
list<T extends ModuleExtension>(type: T["type"]): readonly T[] {
|
|
19
|
+
return this.extensions.filter((item) => item.type === type) as unknown as readonly T[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeOwner(owner: string): void {
|
|
23
|
+
this.extensions = this.extensions.filter((item) => item.owner !== owner);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -82,6 +82,15 @@ export class LazyToolsService extends ToolsService {
|
|
|
82
82
|
return this.tools; // Returns filtered subset
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Definitions remain resolvable even when omitted from the model prompt.
|
|
86
|
+
getTool(name: string): Tool {
|
|
87
|
+
return this.allTools.find(
|
|
88
|
+
(tool) =>
|
|
89
|
+
name &&
|
|
90
|
+
(tool.function.name === name || tool.function.name.endsWith(name))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
/** Rebuild the lazy catalog while preserving enable/disable preferences. */
|
|
86
95
|
resetTools(): void {
|
|
87
96
|
this.allTools = [];
|
|
@@ -155,32 +164,18 @@ export class LazyToolsService extends ToolsService {
|
|
|
155
164
|
};
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
//
|
|
167
|
+
// Prompt visibility is not authorization. Registered functions remain
|
|
168
|
+
// callable without changing which definitions are sent to the model.
|
|
159
169
|
async callTool(
|
|
160
170
|
toolCall: ToolCall,
|
|
161
171
|
enabledTools?: string[],
|
|
162
172
|
callContext?: ToolCallContext
|
|
163
173
|
) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// enabling it via listAvailableTools/enableTools.
|
|
169
|
-
const isCurrentlyEnabled = this.tools.some(
|
|
170
|
-
(t) => t.function.name === functionName
|
|
174
|
+
return super.callTool(
|
|
175
|
+
toolCall,
|
|
176
|
+
enabledTools ?? this.getFunctionNames(),
|
|
177
|
+
callContext
|
|
171
178
|
);
|
|
172
|
-
const existsInAll = this.allTools.some(
|
|
173
|
-
(t) => t.function.name === functionName
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
if (!isCurrentlyEnabled && existsInAll) {
|
|
177
|
-
// Auto-enable by adding the tool name as an exact pattern
|
|
178
|
-
this.enableTools([functionName]);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Always use the current enabled tool names after any auto-enable above,
|
|
182
|
-
// so the base class check sees the freshly-enabled tool in the allowed list.
|
|
183
|
-
return super.callTool(toolCall, this.getToolNames(), callContext);
|
|
184
179
|
}
|
|
185
180
|
|
|
186
181
|
// Internal: Update visible tools based on patterns
|
package/src/services/Tools.ts
CHANGED
|
@@ -135,6 +135,16 @@ export class ToolsService {
|
|
|
135
135
|
return this.tools.map((tool) => tool.function.name);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Return every registered function implementation. Unlike getToolNames(),
|
|
140
|
+
* this is not a prompt-visibility list; lazy tool services may intentionally
|
|
141
|
+
* hide definitions from the model while keeping their functions callable by
|
|
142
|
+
* trusted programmatic consumers such as the script runtime.
|
|
143
|
+
*/
|
|
144
|
+
getFunctionNames(): string[] {
|
|
145
|
+
return Object.keys(this.functions).filter((name) => typeof this.functions[name] === "function");
|
|
146
|
+
}
|
|
147
|
+
|
|
138
148
|
getTool(name: string): Tool {
|
|
139
149
|
return this.tools.find(
|
|
140
150
|
(tool) =>
|
package/src/services/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { TaskRegistry } from "./TaskRegistry";
|
|
|
16
16
|
import { MediaProcessorService } from "./MediaProcessorService";
|
|
17
17
|
import { BehaviorsService } from "./BehaviorsService";
|
|
18
18
|
import { RuntimeReloadService } from "./RuntimeReloadService";
|
|
19
|
+
import { ExtensionsService } from "./ExtensionsService";
|
|
19
20
|
|
|
20
21
|
import { ConversionService } from "./conversion/ConversionService";
|
|
21
22
|
import { TracingService } from "./TracingService";
|
|
@@ -45,6 +46,7 @@ export { TracingService } from "./TracingService";
|
|
|
45
46
|
export type { TracerImpl, SpanHandle } from "./TracingService";
|
|
46
47
|
export * from "./conversion";
|
|
47
48
|
export * from "./RuntimeReloadService";
|
|
49
|
+
export * from "./ExtensionsService";
|
|
48
50
|
export { BehaviorsService } from "./BehaviorsService";
|
|
49
51
|
|
|
50
52
|
let Singletons = {} as {
|
|
@@ -64,6 +66,7 @@ let Singletons = {} as {
|
|
|
64
66
|
Conversion: ConversionService;
|
|
65
67
|
Behaviors: BehaviorsService;
|
|
66
68
|
RuntimeReload: RuntimeReloadService;
|
|
69
|
+
Extensions: ExtensionsService;
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
export const services = (): typeof Singletons => {
|
|
@@ -98,6 +101,7 @@ export const services = (): typeof Singletons => {
|
|
|
98
101
|
knowhowApiClient: new KnowhowSimpleClient(),
|
|
99
102
|
Behaviors,
|
|
100
103
|
RuntimeReload: new RuntimeReloadService(),
|
|
104
|
+
Extensions: new ExtensionsService(),
|
|
101
105
|
};
|
|
102
106
|
|
|
103
107
|
Singletons.Tools.setContext({
|
|
@@ -26,13 +26,15 @@ export interface Region extends Point, Size {}
|
|
|
26
26
|
export type MouseButton = "left" | "right" | "middle";
|
|
27
27
|
|
|
28
28
|
export interface OverlayPrimitive {
|
|
29
|
-
kind: "rect" | "circle" | "line" | "point";
|
|
29
|
+
kind: "rect" | "circle" | "line" | "point" | "text";
|
|
30
30
|
x: number;
|
|
31
31
|
y: number;
|
|
32
32
|
width?: number;
|
|
33
33
|
height?: number;
|
|
34
34
|
x2?: number;
|
|
35
35
|
y2?: number;
|
|
36
|
+
text?: string;
|
|
37
|
+
fontSize?: number;
|
|
36
38
|
color?: string;
|
|
37
39
|
lineWidth?: number;
|
|
38
40
|
}
|
|
@@ -207,6 +207,11 @@ export class ModulesService {
|
|
|
207
207
|
}
|
|
208
208
|
this._loadedModules.push({ modulePath, resolvedPath, module: importedModule, params: initParams });
|
|
209
209
|
|
|
210
|
+
context.Extensions?.removeOwner(modulePath);
|
|
211
|
+
for (const extension of importedModule.extensions ?? []) {
|
|
212
|
+
context.Extensions?.register(modulePath, extension);
|
|
213
|
+
}
|
|
214
|
+
|
|
210
215
|
// Only register tools/agents/plugins/clients if the relevant services
|
|
211
216
|
// are available in context (they may not be during early CLI command registration)
|
|
212
217
|
if (context.Agents) {
|
|
@@ -253,6 +258,7 @@ export class ModulesService {
|
|
|
253
258
|
// Clear first so repeated shutdown/reload attempts cannot destroy twice.
|
|
254
259
|
this._loadedModules = [];
|
|
255
260
|
for (const { modulePath, resolvedPath, module: mod, params } of toDestroy) {
|
|
261
|
+
params.context?.Extensions?.removeOwner(modulePath);
|
|
256
262
|
if (typeof mod.destroy === "function") {
|
|
257
263
|
try {
|
|
258
264
|
await mod.destroy(params);
|
|
@@ -15,6 +15,7 @@ import { EventService } from "../EventService";
|
|
|
15
15
|
import { ConversionService } from "../conversion/ConversionService";
|
|
16
16
|
import { BehaviorsService } from "../BehaviorsService";
|
|
17
17
|
import { ComputerUseService } from "./computerUse";
|
|
18
|
+
import { ExtensionsService, ModuleExtension } from "../ExtensionsService";
|
|
18
19
|
|
|
19
20
|
import { TracingService as TracingServiceType } from "../TracingService";
|
|
20
21
|
/*
|
|
@@ -66,6 +67,7 @@ export interface ModuleContext {
|
|
|
66
67
|
Program?: Command;
|
|
67
68
|
Behaviors?: BehaviorsService;
|
|
68
69
|
ComputerUse?: ComputerUseService;
|
|
70
|
+
Extensions?: ExtensionsService;
|
|
69
71
|
[key: string]: any;
|
|
70
72
|
Tracing?: typeof TracingServiceType;
|
|
71
73
|
}
|
|
@@ -98,6 +100,8 @@ export interface KnowhowModule {
|
|
|
98
100
|
*/
|
|
99
101
|
destroy?: (params: InitParams) => Promise<void>;
|
|
100
102
|
commands: ModuleChatCommand[];
|
|
103
|
+
/** Optional capabilities consumed by core or third-party services. */
|
|
104
|
+
extensions?: ModuleExtension[];
|
|
101
105
|
tools: ModuleTool[];
|
|
102
106
|
agents: ModuleAgent[];
|
|
103
107
|
plugins: ModulePlugin[];
|