Update CLI chat session bridge (#697)

* feat: add CLI chat sessions with Python agent bridge

Introduce a new CLI chat mode that connects Web UI directly to Hermes
Agent's AIAgent via a Python bridge subprocess and Socket.IO, bypassing
the API Server /v1/responses path. Supports streaming, slash commands
(/new, /undo, /retry, /branch, /compress, /save, /title), interrupt,
and steer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat: update CLI chat session bridge

* fix: extend agent bridge startup timeouts

* docs: update bridge chat session design

* feat: align bridge compression and provider registry

* chore: bump version to 0.5.20

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-14 09:03:57 +08:00
committed by GitHub
parent e0fcc0040b
commit eae7195ba8
31 changed files with 3906 additions and 1040 deletions
+51 -1
View File
@@ -17,6 +17,7 @@ export interface StartRunRequest {
session_id?: string
model?: string
queue_id?: string
source?: 'api_server' | 'cli'
}
export interface StartRunResponse {
@@ -77,6 +78,8 @@ const sessionEventHandlers = new Map<string, {
onAbortCompleted: (event: RunEvent) => void
onUsageUpdated: (event: RunEvent) => void
onRunQueued?: (event: RunEvent) => void
onApprovalRequested?: (event: RunEvent) => void
onApprovalResolved?: (event: RunEvent) => void
}>()
/**
@@ -288,6 +291,26 @@ function globalUsageUpdatedHandler(event: RunEvent): void {
}
}
function globalApprovalRequestedHandler(event: RunEvent): void {
const sid = event.session_id
if (!sid) return
const handlers = sessionEventHandlers.get(sid)
if (handlers?.onApprovalRequested) {
handlers.onApprovalRequested(event)
}
}
function globalApprovalResolvedHandler(event: RunEvent): void {
const sid = event.session_id
if (!sid) return
const handlers = sessionEventHandlers.get(sid)
if (handlers?.onApprovalResolved) {
handlers.onApprovalResolved(event)
}
}
/**
* Register event handlers for a session
* @param sessionId - Session ID
@@ -312,6 +335,8 @@ export function registerSessionHandlers(
onAbortCompleted: (event: RunEvent) => void
onUsageUpdated: (event: RunEvent) => void
onRunQueued?: (event: RunEvent) => void
onApprovalRequested?: (event: RunEvent) => void
onApprovalResolved?: (event: RunEvent) => void
}
): () => void {
sessionEventHandlers.set(sessionId, handlers)
@@ -330,6 +355,19 @@ export function unregisterSessionHandlers(sessionId: string): void {
sessionEventHandlers.delete(sessionId)
}
export function respondToolApproval(
sessionId: string,
approvalId: string,
choice: 'once' | 'session' | 'always' | 'deny',
): void {
const socket = connectChatRun()
socket.emit('approval.respond', {
session_id: sessionId,
approval_id: approvalId,
choice,
})
}
export function getChatRunSocket(): Socket | null {
return chatRunSocket
}
@@ -365,7 +403,9 @@ export function connectChatRun(): Socket {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000,
reconnectionDelayMax: 30000,
randomizationFactor: 0.5,
timeout: 30000,
})
// Register global listeners only once per socket connection
@@ -385,6 +425,8 @@ export function connectChatRun(): Socket {
chatRunSocket.on('run.failed', globalRunFailedHandler)
chatRunSocket.on('run.completed', globalRunCompletedHandler)
chatRunSocket.on('run.queued', globalRunQueuedHandler)
chatRunSocket.on('approval.requested', globalApprovalRequestedHandler)
chatRunSocket.on('approval.resolved', globalApprovalResolvedHandler)
// Compression events
chatRunSocket.on('compression.started', globalCompressionStartedHandler)
@@ -527,6 +569,14 @@ export function startRunViaSocket(
if (closed) return
onEvent(evt)
},
onApprovalRequested: (evt: RunEvent) => {
if (closed) return
onEvent(evt)
},
onApprovalResolved: (evt: RunEvent) => {
if (closed) return
onEvent(evt)
},
}
// Register handlers in the global session map
+3 -2
View File
@@ -66,11 +66,13 @@ export function connectGroupChat(opts?: { userId?: string; userName?: string; de
name: opts?.userName || localStorage.getItem('gc_user_name') || undefined,
description: opts?.description || localStorage.getItem('gc_user_description') || undefined,
},
transports: ['websocket'],
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
randomizationFactor: 0.5,
timeout: 30000,
})
return socket
@@ -185,4 +187,3 @@ export async function forceCompress(roomId: string): Promise<{ success: boolean;
method: 'POST',
})
}