ai-whisper 0.9.0 → 0.11.0
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/README.md +1 -1
- package/dist/bin/broker-daemon.js +267 -25
- package/dist/bin/companion-agent.js +58 -7
- package/dist/bin/relay-monitor.js +44 -6
- package/dist/bin/whisper.js +660 -77
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ You pair any two of four agents — `claude`, `codex`, `ezio`, and `agy`. ai-whi
|
|
|
59
59
|
- **ezio** *(optional)* — bundled with ai-whisper; mount it with `whisper collab mount ezio`, no separate install.
|
|
60
60
|
- **agy** *(optional)* — the `agy` command (Antigravity CLI), signed in. Mount it with `whisper collab mount agy` (manual-mount parity; no auto-launch via `collab start`).
|
|
61
61
|
- **Node.js 22+**.
|
|
62
|
-
- **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. See [Evaluator configuration](docs/evaluator-configuration.md).
|
|
62
|
+
- **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. It supports four providers: **Anthropic** (default), a local **Ollama** model, **OpenAI** (and any OpenAI-compatible backend via a `baseURL` — Azure, OpenRouter, vLLM, LM Studio, …), or reusing an **already-mounted agent CLI** (`claude`/`codex`/`agy`) in non-interactive mode (no separate key — it reuses that CLI's own auth). See [Evaluator configuration](docs/evaluator-configuration.md).
|
|
63
63
|
- **tmux** *(optional)* — only for `whisper collab start`, which auto-arranges both agents into panes. The mount flow below does not need it.
|
|
64
64
|
|
|
65
65
|
> **Platform support:** ai-whisper is terminal-native and Unix-oriented — it drives interactive PTY sessions, so it runs on **macOS and Linux**. It is **not supported natively on Windows**: `whisper collab mount` / `reconnect` require a Unix tty-backed shell and will exit with an error pointing here. On Windows, run ai-whisper inside **[WSL2](https://learn.microsoft.com/windows/wsl/install)** — install Node, your agent CLI, and ai-whisper inside the WSL2 distro and run the commands there, where everything works as-is.
|
|
@@ -633,6 +633,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
|
|
|
633
633
|
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
|
|
634
634
|
return row.n;
|
|
635
635
|
}
|
|
636
|
+
var COUNTED_STATUSES = ["done", "halted"];
|
|
637
|
+
function getHandsOffStats(db) {
|
|
638
|
+
const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
|
|
639
|
+
const rows = db.prepare(`SELECT status, created_at, updated_at
|
|
640
|
+
FROM workflows
|
|
641
|
+
WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
|
|
642
|
+
const stats = {
|
|
643
|
+
totalMs: 0,
|
|
644
|
+
count: 0,
|
|
645
|
+
byStatus: {
|
|
646
|
+
done: { count: 0, totalMs: 0 },
|
|
647
|
+
halted: { count: 0, totalMs: 0 }
|
|
648
|
+
},
|
|
649
|
+
earliestKickoffAt: null,
|
|
650
|
+
skipped: 0
|
|
651
|
+
};
|
|
652
|
+
for (const row of rows) {
|
|
653
|
+
const start = Date.parse(row.created_at);
|
|
654
|
+
const end = Date.parse(row.updated_at);
|
|
655
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
656
|
+
stats.skipped += 1;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const elapsed = Math.max(0, end - start);
|
|
660
|
+
stats.totalMs += elapsed;
|
|
661
|
+
stats.count += 1;
|
|
662
|
+
stats.byStatus[row.status].count += 1;
|
|
663
|
+
stats.byStatus[row.status].totalMs += elapsed;
|
|
664
|
+
if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
|
|
665
|
+
stats.earliestKickoffAt = row.created_at;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return stats;
|
|
669
|
+
}
|
|
636
670
|
|
|
637
671
|
// ../broker/dist/runtime/workspace-snapshot.js
|
|
638
672
|
import { execFileSync } from "node:child_process";
|
|
@@ -4352,6 +4386,9 @@ function createControlService(db, events) {
|
|
|
4352
4386
|
...now !== void 0 ? { now } : {}
|
|
4353
4387
|
});
|
|
4354
4388
|
},
|
|
4389
|
+
getHandsOffStats() {
|
|
4390
|
+
return getHandsOffStats(db);
|
|
4391
|
+
},
|
|
4355
4392
|
listRunCostRows(collabId2, workflowId) {
|
|
4356
4393
|
return listRunCostRows(db, { collabId: collabId2, workflowId });
|
|
4357
4394
|
},
|
|
@@ -5310,19 +5347,20 @@ async function sweepStaleBrokerDaemons(input) {
|
|
|
5310
5347
|
}
|
|
5311
5348
|
return { deleted };
|
|
5312
5349
|
}
|
|
5313
|
-
function
|
|
5350
|
+
function isPidAlive(pid) {
|
|
5314
5351
|
try {
|
|
5315
5352
|
process.kill(pid, 0);
|
|
5316
|
-
return
|
|
5353
|
+
return true;
|
|
5317
5354
|
} catch (err) {
|
|
5318
5355
|
const code = err.code;
|
|
5319
|
-
if (code === "ESRCH")
|
|
5320
|
-
return Promise.resolve({ alive: false, startTime: null });
|
|
5321
5356
|
if (code === "EPERM")
|
|
5322
|
-
return
|
|
5323
|
-
return
|
|
5357
|
+
return true;
|
|
5358
|
+
return false;
|
|
5324
5359
|
}
|
|
5325
5360
|
}
|
|
5361
|
+
function defaultIsAlive(pid) {
|
|
5362
|
+
return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
|
|
5363
|
+
}
|
|
5326
5364
|
|
|
5327
5365
|
// ../broker/dist/runtime/daemon-heartbeat.js
|
|
5328
5366
|
function createDaemonHeartbeat(input) {
|
|
@@ -5918,7 +5956,45 @@ function createWorkflowEventBridge(input) {
|
|
|
5918
5956
|
import * as crypto3 from "node:crypto";
|
|
5919
5957
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5920
5958
|
import { Ollama } from "ollama";
|
|
5959
|
+
import OpenAI from "openai";
|
|
5921
5960
|
import { z as z18 } from "zod";
|
|
5961
|
+
|
|
5962
|
+
// src/runtime/agent-cli-presets.ts
|
|
5963
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
5964
|
+
import { accessSync, constants, statSync as statSync3 } from "node:fs";
|
|
5965
|
+
import { delimiter, join as join6 } from "node:path";
|
|
5966
|
+
var defaultSpawn = (command, args, options) => nodeSpawn(command, [...args], options);
|
|
5967
|
+
var PRESETS = {
|
|
5968
|
+
claude: { executable: "claude", execArgs: ["-p"], promptVia: "arg" },
|
|
5969
|
+
codex: { executable: "codex", execArgs: ["exec"], promptVia: "arg" },
|
|
5970
|
+
agy: { executable: "agy", execArgs: ["-p"], promptVia: "arg" }
|
|
5971
|
+
};
|
|
5972
|
+
function resolveAgentCliInvocation(input) {
|
|
5973
|
+
const preset = PRESETS[input.agent];
|
|
5974
|
+
return {
|
|
5975
|
+
executable: input.executable ?? preset.executable,
|
|
5976
|
+
execArgs: input.execArgs ?? preset.execArgs,
|
|
5977
|
+
promptVia: input.promptVia ?? preset.promptVia
|
|
5978
|
+
};
|
|
5979
|
+
}
|
|
5980
|
+
function isExecutableOnPath(executable) {
|
|
5981
|
+
const isExecFile = (p) => {
|
|
5982
|
+
try {
|
|
5983
|
+
if (!statSync3(p).isFile()) return false;
|
|
5984
|
+
accessSync(p, constants.X_OK);
|
|
5985
|
+
return true;
|
|
5986
|
+
} catch {
|
|
5987
|
+
return false;
|
|
5988
|
+
}
|
|
5989
|
+
};
|
|
5990
|
+
if (executable.includes("/") || executable.includes("\\")) return isExecFile(executable);
|
|
5991
|
+
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
5992
|
+
const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
5993
|
+
return dirs.some((dir) => exts.some((ext) => isExecFile(join6(dir, executable + ext))));
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5996
|
+
// src/runtime/relay-orchestrator-evaluator.ts
|
|
5997
|
+
var defaultOpenAIFactory = (opts) => new OpenAI(opts);
|
|
5922
5998
|
var baseFields = {
|
|
5923
5999
|
confidence: z18.number().min(0).max(1),
|
|
5924
6000
|
reason: z18.string()
|
|
@@ -6158,7 +6234,10 @@ function selectBranch(payload) {
|
|
|
6158
6234
|
}
|
|
6159
6235
|
return legacyBranch;
|
|
6160
6236
|
}
|
|
6237
|
+
var ProviderUnavailableError = class extends Error {
|
|
6238
|
+
};
|
|
6161
6239
|
function isProviderUnavailableError(error) {
|
|
6240
|
+
if (error instanceof ProviderUnavailableError) return true;
|
|
6162
6241
|
if (!(error instanceof Error)) return false;
|
|
6163
6242
|
const code = error.code;
|
|
6164
6243
|
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "ECONNRESET")
|
|
@@ -6222,6 +6301,77 @@ function buildOllamaCaller(config) {
|
|
|
6222
6301
|
return { raw: response.message.content };
|
|
6223
6302
|
};
|
|
6224
6303
|
}
|
|
6304
|
+
function buildOpenAICaller(config) {
|
|
6305
|
+
const client = (config.createClient ?? defaultOpenAIFactory)({
|
|
6306
|
+
apiKey: config.apiKey,
|
|
6307
|
+
...config.baseURL ? { baseURL: config.baseURL } : {}
|
|
6308
|
+
});
|
|
6309
|
+
const model = config.model;
|
|
6310
|
+
return async function(systemPrompt, payload, jsonSchema) {
|
|
6311
|
+
const response = await client.chat.completions.create({
|
|
6312
|
+
model,
|
|
6313
|
+
messages: [
|
|
6314
|
+
{ role: "system", content: systemPrompt },
|
|
6315
|
+
{ role: "user", content: JSON.stringify(payload) }
|
|
6316
|
+
],
|
|
6317
|
+
// Non-strict json_schema: schema-shaped guidance only; branch.parse() is authoritative.
|
|
6318
|
+
// `strict` is intentionally omitted (see spec §5.2.1) — do not set it true.
|
|
6319
|
+
response_format: { type: "json_schema", json_schema: { name: "verdict", schema: jsonSchema } },
|
|
6320
|
+
temperature: 0.3
|
|
6321
|
+
});
|
|
6322
|
+
const raw = response.choices[0]?.message.content ?? "";
|
|
6323
|
+
return {
|
|
6324
|
+
raw,
|
|
6325
|
+
...typeof response.usage?.prompt_tokens === "number" ? { inputTokens: response.usage.prompt_tokens } : {},
|
|
6326
|
+
...typeof response.usage?.completion_tokens === "number" ? { outputTokens: response.usage.completion_tokens } : {}
|
|
6327
|
+
};
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
function buildAgentCliCaller(config) {
|
|
6331
|
+
const { executable, execArgs, promptVia } = resolveAgentCliInvocation(config);
|
|
6332
|
+
const spawnImpl = config.spawnImpl ?? defaultSpawn;
|
|
6333
|
+
return function(systemPrompt, payload) {
|
|
6334
|
+
const prompt = `${systemPrompt}
|
|
6335
|
+
|
|
6336
|
+
=== INPUT (JSON) ===
|
|
6337
|
+
${JSON.stringify(payload)}
|
|
6338
|
+
=== END INPUT ===
|
|
6339
|
+
|
|
6340
|
+
Reply with ONLY the JSON object described above \u2014 no prose, no code fence.`;
|
|
6341
|
+
return new Promise((resolve2, reject) => {
|
|
6342
|
+
const child = promptVia === "arg" ? spawnImpl(executable, [...execArgs, prompt], { stdio: ["ignore", "pipe", "pipe"] }) : spawnImpl(executable, execArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
6343
|
+
let stdout = "";
|
|
6344
|
+
let stderr = "";
|
|
6345
|
+
let settled = false;
|
|
6346
|
+
child.stdout.on("data", (chunk) => {
|
|
6347
|
+
stdout += String(chunk);
|
|
6348
|
+
});
|
|
6349
|
+
child.stderr.on("data", (chunk) => {
|
|
6350
|
+
stderr += String(chunk);
|
|
6351
|
+
});
|
|
6352
|
+
child.on("error", (err) => {
|
|
6353
|
+
if (settled) return;
|
|
6354
|
+
settled = true;
|
|
6355
|
+
reject(new ProviderUnavailableError(`agent-cli spawn failed (${executable}): ${err.message}`));
|
|
6356
|
+
});
|
|
6357
|
+
child.on("close", (code) => {
|
|
6358
|
+
if (settled) return;
|
|
6359
|
+
settled = true;
|
|
6360
|
+
if (code !== 0) {
|
|
6361
|
+
reject(new ProviderUnavailableError(`agent-cli ${executable} exited with code ${code}: ${stderr.trim()}`));
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
resolve2({ raw: stdout });
|
|
6365
|
+
});
|
|
6366
|
+
if (promptVia === "stdin") {
|
|
6367
|
+
child.stdin.on("error", () => {
|
|
6368
|
+
});
|
|
6369
|
+
child.stdin.write(prompt);
|
|
6370
|
+
child.stdin.end();
|
|
6371
|
+
}
|
|
6372
|
+
});
|
|
6373
|
+
};
|
|
6374
|
+
}
|
|
6225
6375
|
function buildSingleProviderCaller(config) {
|
|
6226
6376
|
if (config.provider === "anthropic") {
|
|
6227
6377
|
const call2 = buildAnthropicCaller(config);
|
|
@@ -6264,6 +6414,46 @@ function buildSingleProviderCaller(config) {
|
|
|
6264
6414
|
}
|
|
6265
6415
|
};
|
|
6266
6416
|
}
|
|
6417
|
+
if (config.provider === "openai") {
|
|
6418
|
+
const call2 = buildOpenAICaller(config);
|
|
6419
|
+
return async function(payload, branch) {
|
|
6420
|
+
const started = Date.now();
|
|
6421
|
+
let providerResult;
|
|
6422
|
+
try {
|
|
6423
|
+
providerResult = await call2(branch.systemPrompt, payload, branch.jsonSchema);
|
|
6424
|
+
} catch (callErr) {
|
|
6425
|
+
const providerLatencyMs2 = Date.now() - started;
|
|
6426
|
+
return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
|
|
6427
|
+
}
|
|
6428
|
+
const providerLatencyMs = Date.now() - started;
|
|
6429
|
+
try {
|
|
6430
|
+
const verdict = branch.parse(providerResult.raw);
|
|
6431
|
+
return { ok: true, verdict, raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
|
|
6432
|
+
} catch (parseErr) {
|
|
6433
|
+
return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
|
|
6434
|
+
}
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
if (config.provider === "agent-cli") {
|
|
6438
|
+
const call2 = buildAgentCliCaller(config);
|
|
6439
|
+
return async function(payload, branch) {
|
|
6440
|
+
const started = Date.now();
|
|
6441
|
+
let raw;
|
|
6442
|
+
try {
|
|
6443
|
+
({ raw } = await call2(branch.systemPrompt, payload));
|
|
6444
|
+
} catch (callErr) {
|
|
6445
|
+
const providerLatencyMs2 = Date.now() - started;
|
|
6446
|
+
return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
|
|
6447
|
+
}
|
|
6448
|
+
const providerLatencyMs = Date.now() - started;
|
|
6449
|
+
try {
|
|
6450
|
+
const verdict = branch.parse(raw);
|
|
6451
|
+
return { ok: true, verdict, raw, inputTokens: null, outputTokens: null, providerLatencyMs };
|
|
6452
|
+
} catch (parseErr) {
|
|
6453
|
+
return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw, inputTokens: null, outputTokens: null, providerLatencyMs };
|
|
6454
|
+
}
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6267
6457
|
const call = buildOllamaCaller(config);
|
|
6268
6458
|
return async function(payload, branch) {
|
|
6269
6459
|
const started = Date.now();
|
|
@@ -6710,8 +6900,8 @@ function writeOwnPidToBrokerDaemon(db, input) {
|
|
|
6710
6900
|
}
|
|
6711
6901
|
|
|
6712
6902
|
// src/runtime/evaluator-config.ts
|
|
6713
|
-
import { readFileSync as readFileSync2, statSync as
|
|
6714
|
-
import { join as
|
|
6903
|
+
import { readFileSync as readFileSync2, statSync as statSync4 } from "node:fs";
|
|
6904
|
+
import { join as join7 } from "node:path";
|
|
6715
6905
|
|
|
6716
6906
|
// src/runtime/state-root.ts
|
|
6717
6907
|
import os from "node:os";
|
|
@@ -6745,7 +6935,7 @@ function warnIfLoosePerms(path2) {
|
|
|
6745
6935
|
if (process.platform === "win32") return;
|
|
6746
6936
|
let mode;
|
|
6747
6937
|
try {
|
|
6748
|
-
mode =
|
|
6938
|
+
mode = statSync4(path2).mode;
|
|
6749
6939
|
} catch {
|
|
6750
6940
|
return;
|
|
6751
6941
|
}
|
|
@@ -6773,31 +6963,46 @@ function loadEvaluatorConfig() {
|
|
|
6773
6963
|
const root = getStateRoot();
|
|
6774
6964
|
let dotenv = {};
|
|
6775
6965
|
try {
|
|
6776
|
-
dotenv = parseDotEnv(readFileSync2(
|
|
6966
|
+
dotenv = parseDotEnv(readFileSync2(join7(root, ".env"), "utf8"));
|
|
6777
6967
|
} catch (err) {
|
|
6778
6968
|
if (err.code !== "ENOENT") throw err;
|
|
6779
6969
|
}
|
|
6780
6970
|
const envGet = (k) => process.env[k] ?? dotenv[k];
|
|
6781
|
-
const authPath =
|
|
6971
|
+
const authPath = join7(root, "auth.json");
|
|
6782
6972
|
warnIfLoosePerms(authPath);
|
|
6783
6973
|
const auth = readJsonFile(authPath, "auth.json");
|
|
6784
|
-
const config = readJsonFile(
|
|
6974
|
+
const config = readJsonFile(join7(root, "config.json"), "config.json");
|
|
6785
6975
|
const evalCfg = config?.evaluator ?? {};
|
|
6786
6976
|
const evalOllama = evalCfg.ollama ?? {};
|
|
6787
|
-
const provider =
|
|
6977
|
+
const provider = envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider;
|
|
6978
|
+
const resolvedProvider = provider === "ollama" || provider === "openai" || provider === "agent-cli" ? provider : "anthropic";
|
|
6788
6979
|
const rawFallback = envGet("AI_WHISPER_EVALUATOR_FALLBACK") ?? evalCfg.fallback;
|
|
6789
|
-
const fallback = rawFallback === "anthropic" || rawFallback === "ollama" ? rawFallback : null;
|
|
6980
|
+
const fallback = rawFallback === "anthropic" || rawFallback === "ollama" || rawFallback === "openai" || rawFallback === "agent-cli" ? rawFallback : null;
|
|
6790
6981
|
const apiKey = envGet("ANTHROPIC_API_KEY") ?? auth?.ANTHROPIC_API_KEY ?? null;
|
|
6982
|
+
const evalOpenai = evalCfg.openai ?? {};
|
|
6983
|
+
const openaiKey = envGet("OPENAI_API_KEY") ?? auth?.OPENAI_API_KEY ?? null;
|
|
6984
|
+
const evalAgentCli = evalCfg.agentCli ?? {};
|
|
6985
|
+
const rawAgent = envGet("AI_WHISPER_EVALUATOR_AGENT_CLI_AGENT") ?? evalAgentCli.agent;
|
|
6986
|
+
const agent = rawAgent === "claude" || rawAgent === "codex" || rawAgent === "agy" ? rawAgent : null;
|
|
6791
6987
|
return {
|
|
6792
|
-
provider,
|
|
6988
|
+
provider: resolvedProvider,
|
|
6793
6989
|
fallback,
|
|
6794
|
-
anthropic: {
|
|
6795
|
-
apiKey: apiKey && apiKey.length > 0 ? apiKey : null,
|
|
6796
|
-
model: evalCfg.anthropicModel ?? null
|
|
6797
|
-
},
|
|
6990
|
+
anthropic: { apiKey: apiKey && apiKey.length > 0 ? apiKey : null, model: evalCfg.anthropicModel ?? null },
|
|
6798
6991
|
ollama: {
|
|
6799
6992
|
host: envGet("AI_WHISPER_EVALUATOR_OLLAMA_HOST") ?? evalOllama.host ?? null,
|
|
6800
6993
|
model: envGet("AI_WHISPER_EVALUATOR_OLLAMA_MODEL") ?? evalOllama.model ?? null
|
|
6994
|
+
},
|
|
6995
|
+
openai: {
|
|
6996
|
+
apiKey: openaiKey && openaiKey.length > 0 ? openaiKey : null,
|
|
6997
|
+
model: envGet("AI_WHISPER_EVALUATOR_OPENAI_MODEL") ?? evalOpenai.model ?? null,
|
|
6998
|
+
baseURL: envGet("AI_WHISPER_EVALUATOR_OPENAI_BASE_URL") ?? evalOpenai.baseURL ?? null
|
|
6999
|
+
},
|
|
7000
|
+
agentCli: {
|
|
7001
|
+
agent,
|
|
7002
|
+
executable: evalAgentCli.executable ?? null,
|
|
7003
|
+
execArgs: Array.isArray(evalAgentCli.execArgs) ? evalAgentCli.execArgs : null,
|
|
7004
|
+
promptVia: evalAgentCli.promptVia === "arg" || evalAgentCli.promptVia === "stdin" ? evalAgentCli.promptVia : null,
|
|
7005
|
+
model: evalAgentCli.model ?? null
|
|
6801
7006
|
}
|
|
6802
7007
|
};
|
|
6803
7008
|
}
|
|
@@ -6807,6 +7012,21 @@ function computeEvaluatorStatus(cfg, ctx) {
|
|
|
6807
7012
|
if (cfg.provider === "anthropic" && cfg.anthropic.apiKey === null) {
|
|
6808
7013
|
return "missing_anthropic_key";
|
|
6809
7014
|
}
|
|
7015
|
+
if (cfg.provider === "openai") {
|
|
7016
|
+
if (cfg.openai.apiKey === null) return "missing_openai_key";
|
|
7017
|
+
if (cfg.openai.model === null) return "invalid_config";
|
|
7018
|
+
}
|
|
7019
|
+
if (cfg.provider === "agent-cli") {
|
|
7020
|
+
if (cfg.agentCli.agent === null) return "invalid_config";
|
|
7021
|
+
const { executable } = resolveAgentCliInvocation({
|
|
7022
|
+
agent: cfg.agentCli.agent,
|
|
7023
|
+
executable: cfg.agentCli.executable,
|
|
7024
|
+
execArgs: cfg.agentCli.execArgs,
|
|
7025
|
+
promptVia: cfg.agentCli.promptVia
|
|
7026
|
+
});
|
|
7027
|
+
const exists = (ctx.executableExists ?? isExecutableOnPath)(executable);
|
|
7028
|
+
if (!exists) return "agent_cli_unavailable";
|
|
7029
|
+
}
|
|
6810
7030
|
return "ready";
|
|
6811
7031
|
}
|
|
6812
7032
|
|
|
@@ -6815,7 +7035,9 @@ var NOT_CONFIGURED = {
|
|
|
6815
7035
|
provider: "anthropic",
|
|
6816
7036
|
fallback: null,
|
|
6817
7037
|
anthropic: { apiKey: null, model: null },
|
|
6818
|
-
ollama: { host: null, model: null }
|
|
7038
|
+
ollama: { host: null, model: null },
|
|
7039
|
+
openai: { apiKey: null, model: null, baseURL: null },
|
|
7040
|
+
agentCli: { agent: null, executable: null, execArgs: null, promptVia: null, model: null }
|
|
6819
7041
|
};
|
|
6820
7042
|
function recordEvaluatorStatus(db, input) {
|
|
6821
7043
|
const status = computeEvaluatorStatus(input.resolved ?? NOT_CONFIGURED, {
|
|
@@ -6828,7 +7050,7 @@ function recordEvaluatorStatus(db, input) {
|
|
|
6828
7050
|
|
|
6829
7051
|
// src/bin/broker-daemon.ts
|
|
6830
7052
|
import { execFile as execFile2 } from "node:child_process";
|
|
6831
|
-
import { join as
|
|
7053
|
+
import { join as join8 } from "node:path";
|
|
6832
7054
|
|
|
6833
7055
|
// src/runtime/cli-package-info.ts
|
|
6834
7056
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -6878,7 +7100,7 @@ if (collabId) {
|
|
|
6878
7100
|
});
|
|
6879
7101
|
workflowEventBridge.start();
|
|
6880
7102
|
eventSocket = await createEventSocketServer({
|
|
6881
|
-
socketPath:
|
|
7103
|
+
socketPath: join8(getStateSocketsDir(), `events-${collabId}.sock`),
|
|
6882
7104
|
events: [broker.events, externalEvents],
|
|
6883
7105
|
engineVersion: resolveCliVersion()
|
|
6884
7106
|
});
|
|
@@ -6905,10 +7127,30 @@ function providerConfigFrom(kind) {
|
|
|
6905
7127
|
if (!resolved || resolved.anthropic.apiKey === null) return null;
|
|
6906
7128
|
return { provider: "anthropic", apiKey: resolved.anthropic.apiKey };
|
|
6907
7129
|
}
|
|
7130
|
+
if (kind === "openai") {
|
|
7131
|
+
if (!resolved || resolved.openai.apiKey === null || resolved.openai.model === null) return null;
|
|
7132
|
+
return {
|
|
7133
|
+
provider: "openai",
|
|
7134
|
+
apiKey: resolved.openai.apiKey,
|
|
7135
|
+
model: resolved.openai.model,
|
|
7136
|
+
...resolved.openai.baseURL ? { baseURL: resolved.openai.baseURL } : {}
|
|
7137
|
+
};
|
|
7138
|
+
}
|
|
7139
|
+
if (kind === "ollama") {
|
|
7140
|
+
return {
|
|
7141
|
+
provider: "ollama",
|
|
7142
|
+
...resolved?.ollama.host ? { host: resolved.ollama.host } : {},
|
|
7143
|
+
...resolved?.ollama.model ? { model: resolved.ollama.model } : {}
|
|
7144
|
+
};
|
|
7145
|
+
}
|
|
7146
|
+
if (!resolved || resolved.agentCli.agent === null) return null;
|
|
6908
7147
|
return {
|
|
6909
|
-
provider: "
|
|
6910
|
-
|
|
6911
|
-
...resolved
|
|
7148
|
+
provider: "agent-cli",
|
|
7149
|
+
agent: resolved.agentCli.agent,
|
|
7150
|
+
...resolved.agentCli.executable ? { executable: resolved.agentCli.executable } : {},
|
|
7151
|
+
...resolved.agentCli.execArgs ? { execArgs: resolved.agentCli.execArgs } : {},
|
|
7152
|
+
...resolved.agentCli.promptVia ? { promptVia: resolved.agentCli.promptVia } : {},
|
|
7153
|
+
...resolved.agentCli.model ? { model: resolved.agentCli.model } : {}
|
|
6912
7154
|
};
|
|
6913
7155
|
}
|
|
6914
7156
|
var collab = broker.control.getCollab(collabId);
|
|
@@ -666,6 +666,40 @@ function countActiveWorkflowsForCollab(db, collabId) {
|
|
|
666
666
|
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
|
|
667
667
|
return row.n;
|
|
668
668
|
}
|
|
669
|
+
var COUNTED_STATUSES = ["done", "halted"];
|
|
670
|
+
function getHandsOffStats(db) {
|
|
671
|
+
const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
|
|
672
|
+
const rows = db.prepare(`SELECT status, created_at, updated_at
|
|
673
|
+
FROM workflows
|
|
674
|
+
WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
|
|
675
|
+
const stats = {
|
|
676
|
+
totalMs: 0,
|
|
677
|
+
count: 0,
|
|
678
|
+
byStatus: {
|
|
679
|
+
done: { count: 0, totalMs: 0 },
|
|
680
|
+
halted: { count: 0, totalMs: 0 }
|
|
681
|
+
},
|
|
682
|
+
earliestKickoffAt: null,
|
|
683
|
+
skipped: 0
|
|
684
|
+
};
|
|
685
|
+
for (const row of rows) {
|
|
686
|
+
const start = Date.parse(row.created_at);
|
|
687
|
+
const end = Date.parse(row.updated_at);
|
|
688
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
689
|
+
stats.skipped += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const elapsed = Math.max(0, end - start);
|
|
693
|
+
stats.totalMs += elapsed;
|
|
694
|
+
stats.count += 1;
|
|
695
|
+
stats.byStatus[row.status].count += 1;
|
|
696
|
+
stats.byStatus[row.status].totalMs += elapsed;
|
|
697
|
+
if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
|
|
698
|
+
stats.earliestKickoffAt = row.created_at;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return stats;
|
|
702
|
+
}
|
|
669
703
|
|
|
670
704
|
// ../broker/dist/runtime/workspace-snapshot.js
|
|
671
705
|
import { execFileSync } from "node:child_process";
|
|
@@ -4372,6 +4406,9 @@ function createControlService(db, events) {
|
|
|
4372
4406
|
...now !== void 0 ? { now } : {}
|
|
4373
4407
|
});
|
|
4374
4408
|
},
|
|
4409
|
+
getHandsOffStats() {
|
|
4410
|
+
return getHandsOffStats(db);
|
|
4411
|
+
},
|
|
4375
4412
|
listRunCostRows(collabId, workflowId) {
|
|
4376
4413
|
return listRunCostRows(db, { collabId, workflowId });
|
|
4377
4414
|
},
|
|
@@ -5320,19 +5357,20 @@ async function sweepStaleBrokerDaemons(input) {
|
|
|
5320
5357
|
}
|
|
5321
5358
|
return { deleted };
|
|
5322
5359
|
}
|
|
5323
|
-
function
|
|
5360
|
+
function isPidAlive(pid) {
|
|
5324
5361
|
try {
|
|
5325
5362
|
process.kill(pid, 0);
|
|
5326
|
-
return
|
|
5363
|
+
return true;
|
|
5327
5364
|
} catch (err) {
|
|
5328
5365
|
const code = err.code;
|
|
5329
|
-
if (code === "ESRCH")
|
|
5330
|
-
return Promise.resolve({ alive: false, startTime: null });
|
|
5331
5366
|
if (code === "EPERM")
|
|
5332
|
-
return
|
|
5333
|
-
return
|
|
5367
|
+
return true;
|
|
5368
|
+
return false;
|
|
5334
5369
|
}
|
|
5335
5370
|
}
|
|
5371
|
+
function defaultIsAlive(pid) {
|
|
5372
|
+
return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
|
|
5373
|
+
}
|
|
5336
5374
|
|
|
5337
5375
|
// ../broker/dist/runtime/daemon-heartbeat.js
|
|
5338
5376
|
function createDaemonHeartbeat(input) {
|
|
@@ -7506,6 +7544,8 @@ function haxSpawnEnv(base = process.env, transcriptPath) {
|
|
|
7506
7544
|
const env = { ...base, HAX_EXTRA_SKILLS_DIR: aiEzioGlobalSkillsDir(base) };
|
|
7507
7545
|
if (transcriptPath)
|
|
7508
7546
|
env.HAX_TRANSCRIPT = transcriptPath;
|
|
7547
|
+
if (env.HAX_COMPACT_AUTO === void 0)
|
|
7548
|
+
env.HAX_COMPACT_AUTO = "0";
|
|
7509
7549
|
return env;
|
|
7510
7550
|
}
|
|
7511
7551
|
function spawnHax(options = {}) {
|
|
@@ -7828,7 +7868,9 @@ var Session = class {
|
|
|
7828
7868
|
async spawnAndPump(options) {
|
|
7829
7869
|
this._transcriptPath = options.transcriptPath;
|
|
7830
7870
|
const gen = this.generation;
|
|
7831
|
-
const
|
|
7871
|
+
const overrides = this.options.engineEnvOverrides;
|
|
7872
|
+
const spawnOptions = overrides ? { ...options, env: { ...options.env ?? process.env, ...overrides } } : options;
|
|
7873
|
+
const spawned = (this.options.spawn ?? spawnHax)(spawnOptions);
|
|
7832
7874
|
this.spawned = spawned;
|
|
7833
7875
|
spawned.child.on("exit", (code, signal) => {
|
|
7834
7876
|
if (gen !== this.generation)
|
|
@@ -8861,6 +8903,7 @@ function profileEnv(profile, parentEnv) {
|
|
|
8861
8903
|
env.HAX_REASONING_EFFORT = profile.effort;
|
|
8862
8904
|
else
|
|
8863
8905
|
delete env.HAX_REASONING_EFFORT;
|
|
8906
|
+
env.HAX_COMPACT_AUTO = "1";
|
|
8864
8907
|
return env;
|
|
8865
8908
|
}
|
|
8866
8909
|
function validateProfile(profile, parentEnv) {
|
|
@@ -9948,6 +9991,14 @@ function builtinCommands(listCommands) {
|
|
|
9948
9991
|
{
|
|
9949
9992
|
name: "compact",
|
|
9950
9993
|
summary: "summarize old history and free context",
|
|
9994
|
+
// NOTE (post-hax-sync): upstream hax now ships its own `/compact` slash
|
|
9995
|
+
// command (`agent_compact`) that silently rewrites conversation history
|
|
9996
|
+
// inside the engine. This ezio-owned handler intercepts `/compact` here,
|
|
9997
|
+
// routes it to `ctx.compactor.compactNow()` (the harness-controlled protocol
|
|
9998
|
+
// `compact` control), and returns `action: "handled"` — so the literal text
|
|
9999
|
+
// "/compact" is NEVER forwarded as a raw `submit` to the hax session where
|
|
10000
|
+
// the engine's own slash registry would trigger `agent_compact`.
|
|
10001
|
+
// Do NOT remove this command or change its return path to `action: "submit"`.
|
|
9951
10002
|
run: async (ctx) => {
|
|
9952
10003
|
if (!ctx.compactor) {
|
|
9953
10004
|
ctx.write("compaction unavailable\n");
|
|
@@ -636,6 +636,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
|
|
|
636
636
|
const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
|
|
637
637
|
return row.n;
|
|
638
638
|
}
|
|
639
|
+
var COUNTED_STATUSES = ["done", "halted"];
|
|
640
|
+
function getHandsOffStats(db) {
|
|
641
|
+
const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
|
|
642
|
+
const rows = db.prepare(`SELECT status, created_at, updated_at
|
|
643
|
+
FROM workflows
|
|
644
|
+
WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
|
|
645
|
+
const stats = {
|
|
646
|
+
totalMs: 0,
|
|
647
|
+
count: 0,
|
|
648
|
+
byStatus: {
|
|
649
|
+
done: { count: 0, totalMs: 0 },
|
|
650
|
+
halted: { count: 0, totalMs: 0 }
|
|
651
|
+
},
|
|
652
|
+
earliestKickoffAt: null,
|
|
653
|
+
skipped: 0
|
|
654
|
+
};
|
|
655
|
+
for (const row of rows) {
|
|
656
|
+
const start = Date.parse(row.created_at);
|
|
657
|
+
const end = Date.parse(row.updated_at);
|
|
658
|
+
if (Number.isNaN(start) || Number.isNaN(end)) {
|
|
659
|
+
stats.skipped += 1;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const elapsed = Math.max(0, end - start);
|
|
663
|
+
stats.totalMs += elapsed;
|
|
664
|
+
stats.count += 1;
|
|
665
|
+
stats.byStatus[row.status].count += 1;
|
|
666
|
+
stats.byStatus[row.status].totalMs += elapsed;
|
|
667
|
+
if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
|
|
668
|
+
stats.earliestKickoffAt = row.created_at;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return stats;
|
|
672
|
+
}
|
|
639
673
|
|
|
640
674
|
// ../broker/dist/runtime/workspace-snapshot.js
|
|
641
675
|
import { execFileSync } from "node:child_process";
|
|
@@ -4342,6 +4376,9 @@ function createControlService(db, events) {
|
|
|
4342
4376
|
...now !== void 0 ? { now } : {}
|
|
4343
4377
|
});
|
|
4344
4378
|
},
|
|
4379
|
+
getHandsOffStats() {
|
|
4380
|
+
return getHandsOffStats(db);
|
|
4381
|
+
},
|
|
4345
4382
|
listRunCostRows(collabId2, workflowId) {
|
|
4346
4383
|
return listRunCostRows(db, { collabId: collabId2, workflowId });
|
|
4347
4384
|
},
|
|
@@ -5294,19 +5331,20 @@ async function sweepStaleBrokerDaemons(input) {
|
|
|
5294
5331
|
}
|
|
5295
5332
|
return { deleted };
|
|
5296
5333
|
}
|
|
5297
|
-
function
|
|
5334
|
+
function isPidAlive(pid) {
|
|
5298
5335
|
try {
|
|
5299
5336
|
process.kill(pid, 0);
|
|
5300
|
-
return
|
|
5337
|
+
return true;
|
|
5301
5338
|
} catch (err) {
|
|
5302
5339
|
const code = err.code;
|
|
5303
|
-
if (code === "ESRCH")
|
|
5304
|
-
return Promise.resolve({ alive: false, startTime: null });
|
|
5305
5340
|
if (code === "EPERM")
|
|
5306
|
-
return
|
|
5307
|
-
return
|
|
5341
|
+
return true;
|
|
5342
|
+
return false;
|
|
5308
5343
|
}
|
|
5309
5344
|
}
|
|
5345
|
+
function defaultIsAlive(pid) {
|
|
5346
|
+
return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
|
|
5347
|
+
}
|
|
5310
5348
|
|
|
5311
5349
|
// ../broker/dist/runtime/daemon-heartbeat.js
|
|
5312
5350
|
function createDaemonHeartbeat(input) {
|