Integrate master agent runtime orchestration updates
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
112
src/lib/execution/backends/hermes-backend.ts
Normal file
112
src/lib/execution/backends/hermes-backend.ts
Normal 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;
|
||||
212
src/lib/execution/backends/hermes-config.ts
Normal file
212
src/lib/execution/backends/hermes-config.ts
Normal 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;
|
||||
152
src/lib/execution/backends/hermes-runner.ts
Normal file
152
src/lib/execution/backends/hermes-runner.ts
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface ExecutionImmediateCompletedResult {
|
||||
status: "completed";
|
||||
backendId: string;
|
||||
output: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface ExecutionImmediateFailedResult {
|
||||
|
||||
Reference in New Issue
Block a user