mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-02 10:33:00 +08:00
feat: improve Hermes provider support
This commit is contained in:
@@ -48,15 +48,23 @@ function readStopReason(result) {
|
||||
return result.stopReason || result.stop_reason || result.reason || null;
|
||||
}
|
||||
|
||||
function buildPromptParams(sessionId, command, model) {
|
||||
const params = {
|
||||
function buildPromptParams(sessionId, command) {
|
||||
return {
|
||||
sessionId,
|
||||
prompt: [{ type: 'text', text: command }],
|
||||
};
|
||||
if (model) {
|
||||
params.modelId = model;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function buildSessionSetupParams(sessionId, workingDir) {
|
||||
return {
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
cwd: workingDir,
|
||||
mcpServers: [],
|
||||
};
|
||||
}
|
||||
|
||||
function canLoadSession(connection) {
|
||||
return connection?.initializeResult?.agentCapabilities?.loadSession === true;
|
||||
}
|
||||
|
||||
function findPermissionOption(options, kinds, fallbackOptionIds = []) {
|
||||
@@ -233,7 +241,7 @@ async function spawnHermes(command, options = {}, ws) {
|
||||
};
|
||||
|
||||
try {
|
||||
const resolvedModel = await providerModelsService.resolveResumeModel(PROVIDER, sessionId, requestedModel);
|
||||
await providerModelsService.resolveResumeModel(PROVIDER, sessionId, requestedModel);
|
||||
const connection = await hermesConnectionManager.getConnection(workingDir);
|
||||
activeHermesSessions.set(activeKey, {
|
||||
connection,
|
||||
@@ -278,20 +286,21 @@ async function spawnHermes(command, options = {}, ws) {
|
||||
|
||||
try {
|
||||
let sessionResult;
|
||||
if (sessionId) {
|
||||
if (sessionId && canLoadSession(connection)) {
|
||||
try {
|
||||
sessionResult = await connection.request('session/load', { sessionId, cwd: workingDir });
|
||||
sessionResult = await connection.request('session/load', buildSessionSetupParams(sessionId, workingDir));
|
||||
} catch {
|
||||
sessionResult = { sessionId };
|
||||
}
|
||||
} else {
|
||||
sessionResult = await connection.request('session/new', {
|
||||
cwd: workingDir,
|
||||
});
|
||||
sessionResult = await connection.request('session/new', buildSessionSetupParams(null, workingDir));
|
||||
}
|
||||
|
||||
registerSession(readSessionId(sessionResult) || sessionId, connection);
|
||||
const promptResult = await connection.request('session/prompt', buildPromptParams(capturedSessionId, command, resolvedModel));
|
||||
if (!capturedSessionId) {
|
||||
throw new Error('Hermes ACP did not return a session id.');
|
||||
}
|
||||
const promptResult = await connection.request('session/prompt', buildPromptParams(capturedSessionId, command));
|
||||
const finalSessionId = capturedSessionId || readSessionId(promptResult) || sessionId || activeKey;
|
||||
const stopReason = readStopReason(promptResult) || 'completed';
|
||||
const active = activeHermesSessions.get(finalSessionId) || activeHermesSessions.get(activeKey);
|
||||
|
||||
@@ -19,6 +19,7 @@ class AcpClient extends EventEmitter {
|
||||
this.buffer = '';
|
||||
this.requestHandlers = new Map();
|
||||
this.initialized = false;
|
||||
this.initializeResult = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -54,19 +55,20 @@ class AcpClient extends EventEmitter {
|
||||
}
|
||||
|
||||
this.start();
|
||||
await this.request('initialize', {
|
||||
this.initializeResult = await this.request('initialize', {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
clientInfo: {
|
||||
name: 'CloudCLI',
|
||||
title: 'CloudCLI',
|
||||
version: '1.0.0',
|
||||
},
|
||||
capabilities: {
|
||||
fs: false,
|
||||
terminal: false,
|
||||
session: {
|
||||
requestPermission: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.initialized = true;
|
||||
this.notify('initialized', {});
|
||||
@@ -95,7 +97,7 @@ class AcpClient extends EventEmitter {
|
||||
|
||||
const payload = { jsonrpc: '2.0', id, method, params };
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
this.pending.set(id, { resolve, reject, method, params });
|
||||
this.writeMessage(payload);
|
||||
});
|
||||
}
|
||||
@@ -171,7 +173,13 @@ class AcpClient extends EventEmitter {
|
||||
}
|
||||
this.pending.delete(message.id);
|
||||
if (message.error) {
|
||||
pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
||||
const messageText = message.error.message || JSON.stringify(message.error);
|
||||
const error = new Error(`ACP ${pending.method} failed: ${messageText}`);
|
||||
error.code = message.error.code;
|
||||
error.data = message.error.data;
|
||||
error.method = pending.method;
|
||||
error.params = pending.params;
|
||||
pending.reject(error);
|
||||
} else {
|
||||
pending.resolve(message.result);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,7 @@ import type {
|
||||
ProviderModelsDefinition,
|
||||
ProviderSessionActiveModelChange,
|
||||
} from '@/shared/types.js';
|
||||
import {
|
||||
buildDefaultProviderCurrentActiveModel,
|
||||
readOptionalString,
|
||||
writeProviderSessionActiveModelChange,
|
||||
} from '@/shared/utils.js';
|
||||
import { readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
export const HERMES_CONFIGURED_MODEL = '__hermes_configured_model__';
|
||||
|
||||
@@ -105,24 +101,21 @@ export class HermesProviderModels implements IProviderModels {
|
||||
return HERMES_FALLBACK_MODELS;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: activeModel, label: activeModel },
|
||||
...HERMES_FALLBACK_MODELS.OPTIONS,
|
||||
];
|
||||
|
||||
return {
|
||||
OPTIONS: options,
|
||||
DEFAULT: activeModel,
|
||||
OPTIONS: [
|
||||
{
|
||||
value: HERMES_CONFIGURED_MODEL,
|
||||
label: 'Configured in Hermes',
|
||||
description: `Current Hermes model: ${activeModel}`,
|
||||
},
|
||||
],
|
||||
DEFAULT: HERMES_CONFIGURED_MODEL,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentActiveModel(): Promise<ProviderCurrentActiveModel> {
|
||||
const configured = await this.readConfiguredModel();
|
||||
if (configured) {
|
||||
return { model: configured };
|
||||
}
|
||||
|
||||
return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
|
||||
return { model: configured ?? HERMES_CONFIGURED_MODEL };
|
||||
}
|
||||
|
||||
async changeActiveModel(input: ProviderChangeActiveModelInput): Promise<ProviderSessionActiveModelChange> {
|
||||
@@ -136,7 +129,13 @@ export class HermesProviderModels implements IProviderModels {
|
||||
};
|
||||
}
|
||||
|
||||
return writeProviderSessionActiveModelChange('hermes', input);
|
||||
return {
|
||||
provider: 'hermes',
|
||||
sessionId: input.sessionId,
|
||||
supported: false,
|
||||
changed: false,
|
||||
model: null,
|
||||
};
|
||||
}
|
||||
|
||||
private async readConfiguredModel(): Promise<string | null> {
|
||||
|
||||
@@ -60,6 +60,44 @@ function readEventSessionId(raw: AnyRecord, sessionId: string | null): string |
|
||||
return readOptionalString(raw.sessionId) ?? readOptionalString(raw.session_id) ?? sessionId;
|
||||
}
|
||||
|
||||
function readTextContent(value: unknown): string | null {
|
||||
const direct = readOptionalString(value);
|
||||
if (direct !== undefined) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const parts = value
|
||||
.map((entry) => readTextContent(entry))
|
||||
.filter((entry): entry is string => Boolean(entry?.trim()));
|
||||
return parts.length > 0 ? parts.join('') : null;
|
||||
}
|
||||
|
||||
const record = readObjectRecord(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestedContent = record.content;
|
||||
const nestedText = nestedContent === value ? null : readTextContent(nestedContent);
|
||||
|
||||
return readOptionalString(record.text)
|
||||
?? readOptionalString(record.content)
|
||||
?? nestedText
|
||||
?? readOptionalString(record.delta)
|
||||
?? readOptionalString(record.rawOutput)
|
||||
?? readOptionalString(record.raw_output)
|
||||
?? readOptionalString(record.output)
|
||||
?? null;
|
||||
}
|
||||
|
||||
function readToolPayload(raw: AnyRecord): AnyRecord {
|
||||
return readObjectRecord(raw.toolCall)
|
||||
?? readObjectRecord(raw.tool_call)
|
||||
?? readObjectRecord(raw.tool)
|
||||
?? raw;
|
||||
}
|
||||
|
||||
function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, history = false): NormalizedMessage[] {
|
||||
const envelope = readObjectRecord(rawMessage);
|
||||
if (!envelope) {
|
||||
@@ -75,10 +113,10 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
const baseId = readOptionalString(raw.id) ?? readOptionalString(raw.messageId) ?? readOptionalString(raw.message_id) ?? generateMessageId(PROVIDER);
|
||||
|
||||
if (['agent_message_chunk', 'assistant_message_chunk', 'message_delta', 'text_delta', 'text'].includes(type)) {
|
||||
const content = readOptionalString(raw.content)
|
||||
const content = readTextContent(raw.content)
|
||||
?? readOptionalString(raw.text)
|
||||
?? readOptionalString(raw.delta)
|
||||
?? readOptionalString(readObjectRecord(raw.message)?.content)
|
||||
?? readTextContent(readObjectRecord(raw.message)?.content)
|
||||
?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
@@ -96,9 +134,9 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
|
||||
if (['agent_message', 'assistant_message', 'message'].includes(type)) {
|
||||
const role = readOptionalString(raw.role) === 'user' ? 'user' : 'assistant';
|
||||
const content = readOptionalString(raw.content)
|
||||
const content = readTextContent(raw.content)
|
||||
?? readOptionalString(raw.text)
|
||||
?? readOptionalString(readObjectRecord(raw.message)?.content)
|
||||
?? readTextContent(readObjectRecord(raw.message)?.content)
|
||||
?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
@@ -115,7 +153,7 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
}
|
||||
|
||||
if (['agent_thought_chunk', 'thought_delta', 'thinking', 'reasoning'].includes(type)) {
|
||||
const content = readOptionalString(raw.content) ?? readOptionalString(raw.text) ?? readOptionalString(raw.delta) ?? '';
|
||||
const content = readTextContent(raw.content) ?? readOptionalString(raw.text) ?? readOptionalString(raw.delta) ?? '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
}
|
||||
@@ -130,8 +168,15 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
}
|
||||
|
||||
if (['tool_call', 'tool_use', 'tool_call_start'].includes(type)) {
|
||||
const tool = readObjectRecord(raw.tool);
|
||||
const toolId = readOptionalString(raw.toolCallId) ?? readOptionalString(raw.tool_call_id) ?? readOptionalString(raw.toolId) ?? baseId;
|
||||
const tool = readToolPayload(raw);
|
||||
const toolId = readOptionalString(raw.toolCallId)
|
||||
?? readOptionalString(raw.tool_call_id)
|
||||
?? readOptionalString(raw.toolId)
|
||||
?? readOptionalString(tool.toolCallId)
|
||||
?? readOptionalString(tool.tool_call_id)
|
||||
?? readOptionalString(tool.toolId)
|
||||
?? readOptionalString(tool.id)
|
||||
?? baseId;
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId: eventSessionId,
|
||||
@@ -143,22 +188,51 @@ function normalizeHermesEvent(rawMessage: unknown, sessionId: string | null, his
|
||||
?? readOptionalString(raw.title)
|
||||
?? readOptionalString(raw.name)
|
||||
?? readOptionalString(tool?.name)
|
||||
?? readOptionalString(tool?.title)
|
||||
?? 'Tool',
|
||||
toolInput: raw.rawInput ?? raw.raw_input ?? raw.input ?? raw.arguments ?? raw.params ?? tool?.input ?? {},
|
||||
toolInput: raw.rawInput
|
||||
?? raw.raw_input
|
||||
?? raw.input
|
||||
?? raw.arguments
|
||||
?? raw.params
|
||||
?? tool?.rawInput
|
||||
?? tool?.raw_input
|
||||
?? tool?.input
|
||||
?? tool?.arguments
|
||||
?? {},
|
||||
toolId,
|
||||
})];
|
||||
}
|
||||
|
||||
if (['tool_call_update', 'tool_result', 'tool_call_result', 'tool_call_done'].includes(type)) {
|
||||
const tool = readToolPayload(raw);
|
||||
const content = readTextContent(raw.content)
|
||||
?? readTextContent(raw.rawOutput)
|
||||
?? readTextContent(raw.raw_output)
|
||||
?? readTextContent(raw.output)
|
||||
?? readTextContent(raw.result)
|
||||
?? readTextContent(tool.rawOutput)
|
||||
?? readTextContent(tool.raw_output)
|
||||
?? readTextContent(tool.output)
|
||||
?? readTextContent(tool.result)
|
||||
?? '';
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId: eventSessionId,
|
||||
timestamp,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_result',
|
||||
toolId: readOptionalString(raw.toolCallId) ?? readOptionalString(raw.tool_call_id) ?? readOptionalString(raw.toolId) ?? '',
|
||||
content: formatContent(raw.output ?? raw.result ?? raw.content ?? raw.delta ?? ''),
|
||||
isError: Boolean(raw.error) || raw.status === 'error',
|
||||
toolId: readOptionalString(raw.toolCallId)
|
||||
?? readOptionalString(raw.tool_call_id)
|
||||
?? readOptionalString(raw.toolId)
|
||||
?? readOptionalString(tool.toolCallId)
|
||||
?? readOptionalString(tool.tool_call_id)
|
||||
?? readOptionalString(tool.toolId)
|
||||
?? readOptionalString(tool.id)
|
||||
?? '',
|
||||
content: content || formatContent(raw.delta ?? ''),
|
||||
isError: Boolean(raw.error) || raw.status === 'error' || raw.status === 'failed',
|
||||
toolUseResult: raw.result ?? raw.output ?? raw.rawOutput ?? raw.raw_output ?? tool.result ?? tool.output ?? tool.rawOutput ?? tool.raw_output,
|
||||
})];
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ function resolveResumeSessionId(
|
||||
return resolvedSessionId;
|
||||
}
|
||||
|
||||
function getHermesShellCommand(): string {
|
||||
return (process.env.HERMES_COMMAND_PATH || process.env.HERMES_CLI_PATH || 'hermes')
|
||||
.trim()
|
||||
.split(/\s+/)[0] || 'hermes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves provider command line for plain shell and agent-backed shell modes.
|
||||
*/
|
||||
@@ -161,6 +167,14 @@ function buildShellCommand(
|
||||
return initialCommand || 'opencode';
|
||||
}
|
||||
|
||||
if (provider === 'hermes') {
|
||||
const command = initialCommand || getHermesShellCommand();
|
||||
if (resumeSessionId) {
|
||||
return `${command} --resume "${resumeSessionId}"`;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
const command = initialCommand || 'claude';
|
||||
if (resumeSessionId) {
|
||||
if (os.platform() === 'win32') {
|
||||
@@ -481,6 +495,8 @@ export function handleShellConnection(
|
||||
? 'Gemini'
|
||||
: provider === 'opencode'
|
||||
? 'OpenCode'
|
||||
: provider === 'hermes'
|
||||
? 'Hermes'
|
||||
: 'Claude';
|
||||
welcomeMsg = hasSession && resumeSessionId
|
||||
? `\x1b[36mResuming ${providerName} session ${resumeSessionId} in: ${projectPath}\x1b[0m\r\n`
|
||||
|
||||
@@ -946,7 +946,6 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
const codexModels = (await providerModelsService.getProviderModels('codex')).models;
|
||||
const geminiModels = (await providerModelsService.getProviderModels('gemini')).models;
|
||||
const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
|
||||
const hermesModels = (await providerModelsService.getProviderModels('hermes')).models;
|
||||
|
||||
// Start the appropriate session
|
||||
if (provider === 'claude') {
|
||||
@@ -1006,7 +1005,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
||||
projectPath: finalProjectPath,
|
||||
cwd: finalProjectPath,
|
||||
sessionId: sessionId || null,
|
||||
model: model || (hermesModels.DEFAULT === HERMES_CONFIGURED_MODEL ? undefined : hermesModels.DEFAULT)
|
||||
model: model === HERMES_CONFIGURED_MODEL ? undefined : model
|
||||
}, writer);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user