Integrate master agent runtime orchestration updates

This commit is contained in:
kris
2026-04-16 04:41:46 +08:00
parent e0c0ea1814
commit 39be49630f
81 changed files with 9283 additions and 448 deletions

View File

@@ -8,6 +8,11 @@ import {
type ClawBackendSelectionState,
isClawRequestKindSupported,
} from "@/lib/execution/backends/claw-backend";
import {
HERMES_BACKEND,
type HermesBackendSelectionState,
isHermesRequestKindSupported,
} from "@/lib/execution/backends/hermes-backend";
import {
MASTER_CODEX_NODE_BACKEND,
isReadyMasterCodexNodeBackend,
@@ -27,10 +32,12 @@ export interface ExecutionBackendSelectionInput {
requestKind?: ExecutionRequestKind;
requestedBackendId?: string;
claw?: ClawBackendSelectionState;
hermes?: HermesBackendSelectionState;
}
export type ExecutionBackendChoice =
| typeof CLAW_BACKEND
| typeof HERMES_BACKEND
| typeof MASTER_CODEX_NODE_BACKEND
| typeof OPENAI_BACKEND
| typeof ALIYUN_QWEN_BACKEND;
@@ -57,6 +64,14 @@ function isReadyBackend(choice: ExecutionBackendChoice, input: ExecutionBackendS
return isClawRequestKindSupported(requestKind);
}
if (choice.backendId === HERMES_BACKEND.backendId) {
const requestKind = input.requestKind;
if (!input.hermes?.selectable || !requestKind) {
return false;
}
return isHermesRequestKindSupported(requestKind);
}
const candidates = [
...(input.primary.provider === choice.provider ? [input.primary] : []),
...input.backups.filter((item) => item.provider === choice.provider),
@@ -104,6 +119,13 @@ export function listExecutionBackendChoices(
pushBackend(CLAW_BACKEND);
}
if (
input.requestedBackendId === HERMES_BACKEND.backendId &&
isReadyBackend(HERMES_BACKEND, input)
) {
pushBackend(HERMES_BACKEND);
}
if (input.primary.status === "ready") {
pushBackend(primaryBackend);
}

View File

@@ -0,0 +1,112 @@
import type { ExecutionBackend } from "@/lib/execution/execution-backend";
import type {
ExecutionBackendDescription,
ExecutionImmediateResult,
ExecutionRequest,
ExecutionRequestKind,
} from "@/lib/execution/types";
import {
getHermesBackendAvailability,
getHermesBackendConfig,
isHermesBackendConfigured,
type HermesBackendAvailability,
type HermesBackendConfig,
} from "@/lib/execution/backends/hermes-config";
import { runHermesCommand } from "@/lib/execution/backends/hermes-runner";
export const HERMES_BACKEND_ID = "hermes-runtime";
export const HERMES_BACKEND = {
backendId: HERMES_BACKEND_ID,
label: "Hermes Runtime",
mode: "local",
} as const satisfies ExecutionBackendDescription;
const SUPPORTED_HERMES_KINDS = new Set<ExecutionRequestKind>([
"master_agent_reply",
"thread_reply",
]);
type HermesRunnerInput = Parameters<typeof runHermesCommand>[0];
type HermesRunner = (input: HermesRunnerInput) => Promise<ExecutionImmediateResult>;
export interface HermesBackendSelectionState {
enabled: boolean;
selectable: boolean;
availability: HermesBackendAvailability;
supportsKinds: ExecutionRequestKind[];
}
function createFailedResult(error: string): ExecutionImmediateResult {
return {
status: "failed",
backendId: HERMES_BACKEND_ID,
error,
};
}
export function isHermesRequestKindSupported(kind: ExecutionRequestKind) {
return SUPPORTED_HERMES_KINDS.has(kind);
}
export async function getHermesBackendSelectionState(
config: HermesBackendConfig = getHermesBackendConfig(),
): Promise<HermesBackendSelectionState> {
const availability = await getHermesBackendAvailability(config);
return {
enabled: isHermesBackendConfigured(config),
selectable: availability.selectable,
availability,
supportsKinds: [...SUPPORTED_HERMES_KINDS],
};
}
function buildHermesPayload(input: ExecutionRequest, config: HermesBackendConfig) {
return {
kind: input.kind,
projectId: input.projectId,
requestMessageId: input.requestMessageId,
body: input.body,
executionPrompt: input.executionPrompt ?? input.body,
model: input.modelOverride ?? config.defaultModel,
reasoningEffort: input.reasoningEffortOverride ?? "medium",
toolsets: config.toolsets,
skills: config.skills,
...(input.targetProjectId ? { targetProjectId: input.targetProjectId } : {}),
...(input.targetThreadId ? { targetThreadId: input.targetThreadId } : {}),
...(input.requestedByAccount ? { requestedByAccount: input.requestedByAccount } : {}),
...(input.requestedByLabel ? { requestedByLabel: input.requestedByLabel } : {}),
...(input.taskId ? { taskId: input.taskId } : {}),
};
}
export function createHermesBackend(options?: {
config?: HermesBackendConfig;
runner?: HermesRunner;
}): ExecutionBackend {
const config = options?.config ?? getHermesBackendConfig();
const runner = options?.runner ?? runHermesCommand;
return {
backendId: HERMES_BACKEND_ID,
async canHandle(input) {
return isHermesBackendConfigured(config) && isHermesRequestKindSupported(input.kind);
},
async execute(input) {
const canHandle = await this.canHandle(input);
if (!canHandle) {
return createFailedResult("HERMES_BACKEND_NOT_AVAILABLE");
}
return runner({
config,
payload: buildHermesPayload(input, config),
});
},
async describe() {
return HERMES_BACKEND;
},
};
}
export const HERMES_BACKEND_ADAPTER = createHermesBackend();
export const createHermesBackendForTesting = createHermesBackend;

View File

@@ -0,0 +1,212 @@
import { constants } from "node:fs";
import { access } from "node:fs/promises";
import path from "node:path";
export interface HermesBackendConfig {
enabled: boolean;
command: string;
args: string[];
cwd?: string;
timeoutMs: number;
defaultModel?: string;
toolsets: string[];
skills: string[];
sourceTag: string;
}
export type HermesBackendAvailabilityStatus = "disabled" | "misconfigured" | "ready";
export interface HermesBackendAvailability {
status: HermesBackendAvailabilityStatus;
selectable: boolean;
configured: boolean;
reason:
| "disabled"
| "command_not_set"
| "command_not_found"
| "workdir_not_found"
| "script_not_found"
| "ready";
reasonLabel: string;
command?: string;
cwd?: string;
}
function parseBoolean(value: string | undefined) {
return value?.trim().toLowerCase() === "true";
}
function parseArgs(value: string | undefined) {
return String(value || "")
.trim()
.split(/\s+/)
.filter(Boolean);
}
function parseCsv(value: string | undefined) {
return String(value || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function parseTimeoutMs(value: string | undefined) {
const parsed = Number.parseInt(value || "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120000;
}
export function getHermesBackendConfig(): HermesBackendConfig {
return {
enabled: parseBoolean(process.env.BOSS_HERMES_ENABLED),
command: process.env.BOSS_HERMES_COMMAND?.trim() || "hermes",
args: parseArgs(process.env.BOSS_HERMES_ARGS),
cwd: process.env.BOSS_HERMES_WORKDIR?.trim() || undefined,
timeoutMs: parseTimeoutMs(process.env.BOSS_HERMES_TIMEOUT_MS),
defaultModel: process.env.BOSS_HERMES_DEFAULT_MODEL?.trim() || undefined,
toolsets: parseCsv(process.env.BOSS_HERMES_TOOLSETS),
skills: parseCsv(process.env.BOSS_HERMES_SKILLS),
sourceTag: process.env.BOSS_HERMES_SOURCE_TAG?.trim() || "tool",
};
}
export function isHermesBackendConfigured(config: HermesBackendConfig) {
return config.enabled && Boolean(config.command);
}
function commandLooksLikePath(command: string) {
return command.includes("/") || command.includes("\\");
}
async function fileExists(filePath: string, mode = constants.F_OK) {
try {
await access(filePath, mode);
return true;
} catch {
return false;
}
}
function resolveScriptCandidate(config: HermesBackendConfig) {
if (!config.command || config.args.length === 0) {
return null;
}
const commandName = path.basename(config.command).toLowerCase();
const scriptRuntimes = new Set([
"node",
"node.exe",
"tsx",
"tsx.cmd",
"bun",
"bun.exe",
"deno",
"deno.exe",
"python",
"python.exe",
"python3",
"python3.exe",
]);
if (!scriptRuntimes.has(commandName)) {
return null;
}
const candidate = config.args[0];
if (!candidate || candidate.startsWith("-")) {
return null;
}
return path.isAbsolute(candidate)
? candidate
: path.resolve(config.cwd?.trim() || process.cwd(), candidate);
}
async function isCommandReachable(command: string) {
if (commandLooksLikePath(command)) {
return fileExists(path.resolve(command), constants.X_OK);
}
const searchPaths = (process.env.PATH || "")
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean);
for (const entry of searchPaths) {
const candidate = path.join(entry, command);
if (await fileExists(candidate, constants.X_OK)) {
return true;
}
}
return false;
}
export async function getHermesBackendAvailability(
config: HermesBackendConfig = getHermesBackendConfig(),
): Promise<HermesBackendAvailability> {
const base = {
command: config.command,
cwd: config.cwd,
configured: isHermesBackendConfigured(config),
};
if (!config.enabled) {
return {
...base,
status: "disabled",
selectable: false,
reason: "disabled",
reasonLabel: "Hermes Runtime 当前未启用。",
};
}
if (!config.command) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "command_not_set",
reasonLabel: "Hermes Runtime 缺少启动命令。",
};
}
if (!(await isCommandReachable(config.command))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "command_not_found",
reasonLabel: "未检测到可执行的 Hermes 启动命令。",
};
}
if (config.cwd && !(await fileExists(config.cwd, constants.F_OK))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "workdir_not_found",
reasonLabel: "Hermes Runtime 工作目录不存在。",
};
}
const scriptCandidate = resolveScriptCandidate(config);
if (scriptCandidate && !(await fileExists(scriptCandidate, constants.F_OK))) {
return {
...base,
status: "misconfigured",
selectable: false,
reason: "script_not_found",
reasonLabel: "未检测到有效的 Hermes 启动脚本,将自动回退到默认后端。",
};
}
return {
...base,
status: "ready",
selectable: true,
reason: "ready",
reasonLabel: "Hermes Runtime 可用。",
};
}
export const getHermesBackendConfigForTesting = getHermesBackendConfig;
export const isHermesBackendConfiguredForTesting = isHermesBackendConfigured;
export const getHermesBackendAvailabilityForTesting = getHermesBackendAvailability;

View File

@@ -0,0 +1,152 @@
import { spawn } from "node:child_process";
import type { HermesBackendConfig } from "@/lib/execution/backends/hermes-config";
import type { ExecutionImmediateResult } from "@/lib/execution/types";
const HERMES_BACKEND_ID = "hermes-runtime";
function createFailedResult(error: string): ExecutionImmediateResult {
return {
status: "failed",
backendId: HERMES_BACKEND_ID,
error,
};
}
function trimHermesQuietFooter(stdout: string) {
return stdout
.split(/\r?\n/)
.filter((line) => !/^session_id:\s*\S+/i.test(line.trim()))
.join("\n")
.trim();
}
function extractHermesSessionId(stdout: string) {
for (const line of stdout.split(/\r?\n/)) {
const match = line.trim().match(/^session_id:\s*(\S+)/i);
if (match?.[1]) {
return match[1];
}
}
return undefined;
}
function normalizeHermesProcessResult(input: {
exitCode: number;
stdout: string;
stderr: string;
}): ExecutionImmediateResult {
if (input.exitCode !== 0) {
return createFailedResult(input.stderr.trim() || `HERMES_EXIT_${input.exitCode}`);
}
const output = trimHermesQuietFooter(input.stdout);
const sessionId = extractHermesSessionId(input.stdout);
if (!output) {
return createFailedResult("EMPTY_HERMES_RESPONSE");
}
return {
status: "completed",
backendId: HERMES_BACKEND_ID,
output,
sessionId,
};
}
function buildHermesArgs(config: HermesBackendConfig, payload: Record<string, unknown>) {
const executionPrompt =
typeof payload.executionPrompt === "string" && payload.executionPrompt.trim()
? payload.executionPrompt.trim()
: typeof payload.body === "string"
? payload.body
: "";
const model =
typeof payload.model === "string" && payload.model.trim()
? payload.model.trim()
: config.defaultModel?.trim();
const args = [
...config.args,
"chat",
"-q",
executionPrompt,
"-Q",
"--source",
config.sourceTag,
];
if (model) {
args.push("-m", model);
}
const toolsets = config.toolsets ?? [];
const skills = config.skills ?? [];
if (toolsets.length > 0) {
args.push("-t", toolsets.join(","));
}
if (skills.length > 0) {
args.push("-s", skills.join(","));
}
return args;
}
export async function runHermesCommand(input: {
config: HermesBackendConfig;
payload: Record<string, unknown>;
}): Promise<ExecutionImmediateResult> {
const command = input.config.command;
if (!command) {
return createFailedResult("HERMES_COMMAND_NOT_CONFIGURED");
}
return new Promise((resolve) => {
const child = spawn(command, buildHermesArgs(input.config, input.payload), {
cwd: input.config.cwd,
env: process.env,
stdio: ["ignore", "pipe", "pipe"] as const,
});
let stdout = "";
let stderr = "";
let settled = false;
const finish = (result: ExecutionImmediateResult) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(result);
};
const timer = setTimeout(() => {
child.kill("SIGKILL");
finish(createFailedResult("HERMES_TIMEOUT"));
}, input.config.timeoutMs);
child.stdout.on("data", (chunk: Buffer | string) => {
stdout += String(chunk);
});
child.stderr.on("data", (chunk: Buffer | string) => {
stderr += String(chunk);
});
child.on("error", (error: Error) => {
finish(createFailedResult(error.message));
});
child.on("close", (code: number | null) => {
finish(
normalizeHermesProcessResult({
exitCode: code ?? 1,
stdout,
stderr,
}),
);
});
});
}
export const runHermesCommandForTesting = runHermesCommand;
export const createHermesProcessResultForTesting = normalizeHermesProcessResult;

View File

@@ -5,6 +5,60 @@ export type RelevantMemory = Pick<
"memoryId" | "scope" | "projectId" | "title" | "content" | "tags" | "memoryType" | "lastUsedAt" | "updatedAt" | "createdAt"
>;
function normalizeLexicalText(value: string) {
return value.trim().toLowerCase();
}
function tokenizeLexicalText(value: string) {
const normalized = normalizeLexicalText(value);
if (!normalized) {
return [];
}
return Array.from(
new Set(
normalized
.split(/[^\p{L}\p{N}]+/u)
.map((token) => token.trim())
.filter((token) => token.length >= 2),
),
);
}
function scoreProjectMemoryMatch(memory: RelevantMemory, requestText: string) {
const lowered = normalizeLexicalText(requestText);
if (!lowered) {
return 0;
}
const haystacks = [memory.projectId, memory.title, memory.content, ...(memory.tags ?? [])]
.filter((value): value is string => Boolean(value))
.map((value) => normalizeLexicalText(value));
let score = 0;
for (const value of haystacks) {
if (!value) {
continue;
}
if (lowered.includes(value) || value.includes(lowered)) {
score += 10;
}
}
const requestTokens = tokenizeLexicalText(requestText);
if (requestTokens.length === 0) {
return score;
}
const memoryTokens = new Set(haystacks.flatMap((value) => tokenizeLexicalText(value)));
for (const token of requestTokens) {
if (memoryTokens.has(token)) {
score += 3;
}
}
return score;
}
export function resolveRelevantMemories(input: {
projectId: string;
requestText?: string;
@@ -26,12 +80,13 @@ export function resolveRelevantMemories(input: {
: !lowered
? projectScoped.slice(0, 6)
: projectScoped
.filter((memory) => {
const haystacks = [memory.projectId, memory.title, memory.content, ...(memory.tags ?? [])]
.filter((value): value is string => Boolean(value))
.map((value) => value.toLowerCase());
return haystacks.some((value) => lowered.includes(value) || value.includes(lowered));
})
.map((memory) => ({
memory,
score: scoreProjectMemoryMatch(memory, lowered),
}))
.filter((entry) => entry.score > 0)
.sort((left, right) => right.score - left.score)
.map((entry) => entry.memory)
.slice(0, 6);
const userMemories = input.memories.filter((memory) => memory.scope === "global").slice(0, 8);

View File

@@ -7,6 +7,10 @@ export interface RemoteExecutionResultInput {
replyBody?: string;
errorMessage?: string;
requestId?: string;
warnings?: Array<{
title?: string;
summary?: string;
}>;
}
export interface NormalizedRemoteExecutionResult {
@@ -18,6 +22,10 @@ export interface NormalizedRemoteExecutionResult {
replyBody?: string;
errorMessage?: string;
requestId?: string;
warnings?: Array<{
title: string;
summary: string;
}>;
}
function trimToDefined(value: string | undefined) {
@@ -56,12 +64,28 @@ function buildThreadEnvironmentErrorMessage() {
return "THREAD_ENVIRONMENT_INVALID: 线程返回了内部环境提示,已拦截,请检查线程绑定或工作目录。";
}
function normalizeExecutionWarnings(
warnings: RemoteExecutionResultInput["warnings"],
): NormalizedRemoteExecutionResult["warnings"] {
const normalized = (warnings ?? [])
.map((warning) => ({
title: trimToDefined(warning?.title),
summary: trimToDefined(warning?.summary),
}))
.filter(
(warning): warning is { title: string; summary: string } =>
Boolean(warning.title && warning.summary),
);
return normalized.length > 0 ? normalized : undefined;
}
export function normalizeRemoteExecutionResult(
input: RemoteExecutionResultInput,
): NormalizedRemoteExecutionResult {
const rawThreadReply = trimToDefined(input.rawThreadReply);
const replyBody = trimToDefined(input.replyBody);
const errorMessage = trimToDefined(input.errorMessage);
const warnings = normalizeExecutionWarnings(input.warnings);
const hasEnvironmentDiagnostic =
looksLikeThreadEnvironmentDiagnostic(rawThreadReply) ||
looksLikeThreadEnvironmentDiagnostic(replyBody);
@@ -74,6 +98,7 @@ export function normalizeRemoteExecutionResult(
targetThreadId: trimToDefined(input.targetThreadId),
errorMessage: errorMessage || buildThreadEnvironmentErrorMessage(),
requestId: trimToDefined(input.requestId),
warnings,
};
}
@@ -86,6 +111,7 @@ export function normalizeRemoteExecutionResult(
replyBody,
errorMessage,
requestId: trimToDefined(input.requestId),
warnings,
};
}

View File

@@ -33,6 +33,7 @@ export interface ExecutionImmediateCompletedResult {
status: "completed";
backendId: string;
output: string;
sessionId?: string;
}
export interface ExecutionImmediateFailedResult {