switch hermes upstream to acp prompt streaming

This commit is contained in:
Haitao Pan 2026-04-22 15:03:33 +08:00
parent a279ecffe2
commit 060648834b
2 changed files with 57 additions and 152 deletions

View File

@ -1,7 +1,6 @@
package hermesadapter
import (
"context"
"encoding/json"
"errors"
"flag"
@ -31,7 +30,6 @@ type Server struct {
providerLabel string
allowedOrigins []string
upstreamMethod string
sessionRunner func(context.Context, string, string, string) (string, error)
sessionsMu sync.Mutex
sessions map[string]*adapterSession
}
@ -112,17 +110,8 @@ func NewServer(client rpcClient) *Server {
providerID: strings.TrimSpace(shared.EnvOrDefault("HERMES_ADAPTER_PROVIDER_ID", defaultProviderID)),
providerLabel: strings.TrimSpace(shared.EnvOrDefault("HERMES_ADAPTER_PROVIDER_LABEL", defaultLabel)),
allowedOrigins: parseAllowedOrigins(strings.TrimSpace(shared.EnvOrDefault("HERMES_ADAPTER_ALLOWED_ORIGINS", "https://xworkmate.svc.plus,http://localhost:*,http://127.0.0.1:*"))),
upstreamMethod: strings.TrimSpace(shared.EnvOrDefault("HERMES_ADAPTER_UPSTREAM_METHOD", "session.start")),
sessionRunner: func(ctx context.Context, model, prompt, workingDirectory string) (string, error) {
return shared.RunProviderCommand(
ctx,
defaultProviderID,
model,
prompt,
workingDirectory,
)
},
sessions: make(map[string]*adapterSession),
upstreamMethod: strings.TrimSpace(shared.EnvOrDefault("HERMES_ADAPTER_UPSTREAM_METHOD", "session/prompt")),
sessions: make(map[string]*adapterSession),
}
}
@ -275,12 +264,18 @@ func (s *Server) handleSessionRequest(method string, params map[string]any) map[
if upstreamMethod != "" {
return s.handleConfiguredUpstreamSessionRequest(method, upstreamMethod, params)
}
return s.handleCompatSessionRequest(method, params)
return map[string]any{
"success": false,
"provider": s.providerID,
"mode": "single-agent",
"error": "hermes upstream method is not configured",
}
}
func (s *Server) handleConfiguredUpstreamSessionRequest(method, upstreamMethod string, params map[string]any) map[string]any {
if strings.TrimSpace(strings.ToLower(upstreamMethod)) == "prompt" {
return s.handleHermesPromptUpstreamSessionRequest(method, params)
switch normalizeHermesUpstreamMethod(upstreamMethod) {
case "", "prompt", "session/start", "session/message", "session/prompt":
return s.handleHermesACPUpstreamSessionRequest(method, params)
}
response, err := s.client.Call(upstreamMethod, params)
if err != nil {
@ -321,7 +316,7 @@ func (s *Server) handleConfiguredUpstreamSessionRequest(method, upstreamMethod s
}
}
func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params map[string]any) map[string]any {
func (s *Server) handleHermesACPUpstreamSessionRequest(method string, params map[string]any) map[string]any {
sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", ""))
if sessionID == "" {
return map[string]any{
@ -356,8 +351,9 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
}
if state.upstreamSessionID == "" || method == "session.start" {
newSessionResp, err := s.client.Call("new_session", map[string]any{
"cwd": workingDirectory,
newSessionResp, err := s.client.Call("session/new", map[string]any{
"cwd": workingDirectory,
"mcpServers": []any{},
})
if err != nil {
return map[string]any{
@ -405,7 +401,7 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
"text": taskPrompt,
},
}
response, err := s.client.Call("prompt", map[string]any{
response, err := s.client.Call("session/prompt", map[string]any{
"sessionId": state.upstreamSessionID,
"prompt": promptPayload,
})
@ -421,11 +417,13 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
output := strings.TrimSpace(strings.Join(outputParts, ""))
if output == "" {
if resultMap, ok := response["result"].(map[string]any); ok {
for _, key := range []string{"output", "finalResponse", "final_response", "text", "message"} {
if candidate := strings.TrimSpace(shared.StringArg(resultMap, key, "")); candidate != "" {
output = candidate
break
for _, key := range []string{"output", "finalResponse", "final_response", "text", "message", "response"} {
candidate := strings.TrimSpace(shared.StringArg(resultMap, key, ""))
if candidate == "" || isGenericHermesAckText(candidate) {
continue
}
output = candidate
break
}
}
}
@ -435,7 +433,7 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
"provider": s.providerID,
"mode": "single-agent",
"error": "hermes upstream returned empty response",
"upstreamMethod": "prompt",
"upstreamMethod": "session/prompt",
"upstream": response,
}
}
@ -448,7 +446,7 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
}
current.history = append(current.history, "USER: "+taskPrompt, "ASSISTANT: "+output)
current.lastOutput = output
current.lastUpstreamMethod = "prompt"
current.lastUpstreamMethod = "session/prompt"
s.sessionsMu.Unlock()
result := map[string]any{
@ -457,7 +455,7 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
"mode": "single-agent",
"output": output,
"sessionId": sessionID,
"upstreamMethod": "prompt",
"upstreamMethod": "session/prompt",
}
if workingDirectory != "" {
result["effectiveWorkingDirectory"] = workingDirectory
@ -468,6 +466,13 @@ func (s *Server) handleHermesPromptUpstreamSessionRequest(method string, params
return result
}
func normalizeHermesUpstreamMethod(method string) string {
normalized := strings.ToLower(strings.TrimSpace(method))
normalized = strings.ReplaceAll(normalized, ".", "/")
normalized = strings.ReplaceAll(normalized, "_", "/")
return normalized
}
func extractHermesUpstreamSessionID(response map[string]any) string {
for _, key := range []string{"sessionId", "session_id", "id"} {
if value := strings.TrimSpace(shared.StringArg(asMap(response["result"]), key, "")); value != "" {
@ -484,6 +489,10 @@ func extractHermesSessionUpdateText(notification map[string]any) string {
if notification == nil {
return ""
}
method := strings.TrimSpace(shared.StringArg(notification, "method", ""))
if method != "session.update" && method != "session/update" && method != "acp.session.update" {
return ""
}
payload := asMap(notification["params"])
if len(payload) == 0 {
payload = notification
@ -545,6 +554,15 @@ func extractHermesTextValue(value any) string {
}
}
func isGenericHermesAckText(text string) bool {
switch strings.ToLower(strings.TrimSpace(text)) {
case "", "ok", "session started", "single-agent completed":
return true
default:
return false
}
}
func asMap(value any) map[string]any {
if value == nil {
return nil
@ -555,91 +573,6 @@ func asMap(value any) map[string]any {
return nil
}
func (s *Server) handleCompatSessionRequest(method string, params map[string]any) map[string]any {
if s.sessionRunner == nil {
return map[string]any{
"success": false,
"provider": s.providerID,
"mode": "single-agent",
"error": "hermes session runner is not configured",
}
}
sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", ""))
if sessionID == "" {
return map[string]any{
"success": false,
"provider": s.providerID,
"mode": "single-agent",
"error": "sessionId is required",
}
}
state := s.getOrCreateSession(sessionID)
if method == "session.start" {
state = s.resetSession(sessionID)
}
taskPrompt := strings.TrimSpace(shared.StringArg(params, "taskPrompt", ""))
taskPrompt = shared.AugmentPromptWithAttachments(taskPrompt, params)
if taskPrompt == "" {
return map[string]any{
"success": false,
"provider": s.providerID,
"mode": "single-agent",
"error": "taskPrompt is required",
}
}
model := strings.TrimSpace(shared.StringArg(params, "model", ""))
if model == "" {
model = state.model
}
workingDirectory := strings.TrimSpace(shared.StringArg(params, "workingDirectory", ""))
if workingDirectory == "" {
workingDirectory = state.workingDirectory
}
sessionsHistory := append([]string(nil), state.history...)
sessionsHistory = append(sessionsHistory, "USER: "+taskPrompt)
composedPrompt := shared.ComposeHistoryPrompt(sessionsHistory)
output, err := s.sessionRunner(context.Background(), model, composedPrompt, workingDirectory)
if err != nil {
return map[string]any{
"success": false,
"provider": s.providerID,
"mode": "single-agent",
"error": err.Error(),
}
}
s.sessionsMu.Lock()
state = s.sessions[sessionID]
if state == nil {
state = &adapterSession{}
s.sessions[sessionID] = state
}
state.history = append(sessionsHistory, "ASSISTANT: "+output)
state.model = model
state.workingDirectory = workingDirectory
state.lastOutput = output
state.lastUpstreamMethod = "prompt"
s.sessionsMu.Unlock()
result := map[string]any{
"success": true,
"provider": s.providerID,
"mode": "single-agent",
"output": output,
"sessionId": sessionID,
"upstreamMethod": "prompt",
}
if workingDirectory != "" {
result["effectiveWorkingDirectory"] = workingDirectory
}
if model != "" {
result["resolvedModel"] = model
}
return result
}
func (s *Server) getOrCreateSession(sessionID string) *adapterSession {
s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()

View File

@ -2,7 +2,6 @@ package hermesadapter
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@ -64,15 +63,16 @@ func TestHandleRPCSessionStartReturnsUpstreamResult(t *testing.T) {
stub = &stubClient{initResult: initializeResult{ProtocolVersion: 1}}
stub.callFn = func(method string, params map[string]any) (map[string]any, error) {
switch method {
case "new_session":
case "session/new":
return map[string]any{
"result": map[string]any{
"sessionId": "upstream-session-1",
},
}, nil
case "prompt":
case "session/prompt":
if stub.notificationHandler != nil {
stub.notificationHandler(map[string]any{
"method": "session/update",
"params": map[string]any{
"update": map[string]any{
"sessionUpdate": "agent_message_chunk",
@ -91,7 +91,7 @@ func TestHandleRPCSessionStartReturnsUpstreamResult(t *testing.T) {
}
}
server := NewServer(stub)
server.upstreamMethod = "prompt"
server.upstreamMethod = "session/prompt"
body, _ := json.Marshal(shared.RPCRequest{
JSONRPC: "2.0",
@ -119,8 +119,8 @@ func TestHandleRPCSessionStartReturnsUpstreamResult(t *testing.T) {
if got := result["output"]; got != "hello" {
t.Fatalf("expected output hello, got %#v", result)
}
if len(stub.methods) != 2 || stub.methods[0] != "new_session" || stub.methods[1] != "prompt" {
t.Fatalf("expected new_session then prompt, got %#v", stub.methods)
if len(stub.methods) != 2 || stub.methods[0] != "session/new" || stub.methods[1] != "session/prompt" {
t.Fatalf("expected session/new then session/prompt, got %#v", stub.methods)
}
}
@ -129,13 +129,13 @@ func TestHandleRPCSessionStartRejectsEmptyUpstreamResponse(t *testing.T) {
stub = &stubClient{initResult: initializeResult{ProtocolVersion: 1}}
stub.callFn = func(method string, params map[string]any) (map[string]any, error) {
switch method {
case "new_session":
case "session/new":
return map[string]any{
"result": map[string]any{
"sessionId": "upstream-session-1",
},
}, nil
case "prompt":
case "session/prompt":
return map[string]any{
"result": map[string]any{},
}, nil
@ -144,7 +144,7 @@ func TestHandleRPCSessionStartRejectsEmptyUpstreamResponse(t *testing.T) {
}
}
server := NewServer(stub)
server.upstreamMethod = "prompt"
server.upstreamMethod = "session/prompt"
body, _ := json.Marshal(shared.RPCRequest{
JSONRPC: "2.0",
@ -177,38 +177,10 @@ func TestHandleRPCSessionStartRejectsEmptyUpstreamResponse(t *testing.T) {
}
}
func TestHandleSessionStartFallsBackToPromptRunner(t *testing.T) {
stub := &stubClient{initResult: initializeResult{ProtocolVersion: 1}}
server := NewServer(stub)
server.upstreamMethod = ""
server.sessionRunner = func(ctx context.Context, model, prompt, workingDirectory string) (string, error) {
if workingDirectory != "/tmp/demo" {
t.Fatalf("expected workingDirectory /tmp/demo, got %q", workingDirectory)
}
expectedPrompt := "## User Turn 1\nReply with exactly pong"
if prompt != expectedPrompt {
t.Fatalf("unexpected prompt %q", prompt)
}
return "pong", nil
}
result := server.handleRequest(shared.RPCRequest{
Method: "session.start",
Params: map[string]any{
"sessionId": "s1",
"taskPrompt": "Reply with exactly pong",
"workingDirectory": "/tmp/demo",
},
})
if got := result["output"]; got != "pong" {
t.Fatalf("expected output pong, got %#v", result)
}
}
func TestNewServerDefaultsHermesToUpstreamPrompt(t *testing.T) {
func TestNewServerDefaultsHermesToSessionPrompt(t *testing.T) {
server := NewServer(&stubClient{})
if got := server.upstreamMethod; got != "session.start" {
t.Fatalf("expected default upstream method session.start, got %q", got)
if got := server.upstreamMethod; got != "session/prompt" {
t.Fatalf("expected default upstream method session/prompt, got %q", got)
}
}