switch hermes upstream to acp prompt streaming
This commit is contained in:
parent
a279ecffe2
commit
060648834b
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user