272 lines
7.9 KiB
Go
272 lines
7.9 KiB
Go
package acp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"xworkmate-bridge/internal/memory"
|
|
"xworkmate-bridge/internal/router"
|
|
"xworkmate-bridge/internal/shared"
|
|
)
|
|
|
|
type SessionOrchestrator struct {
|
|
server *Server
|
|
}
|
|
|
|
func NewSessionOrchestrator(server *Server) *SessionOrchestrator {
|
|
return &SessionOrchestrator{server: server}
|
|
}
|
|
|
|
func (o *SessionOrchestrator) Process(ctx context.Context, method string, params map[string]any, notify func(map[string]any)) (map[string]any, *shared.RPCError) {
|
|
res, err := o.server.routingEngine.Resolve(ctx, params)
|
|
if err != nil {
|
|
if err.Error() == "ROUTING_REQUIRED" {
|
|
return nil, &shared.RPCError{Code: -32602, Message: err.Error()}
|
|
}
|
|
return nil, &shared.RPCError{Code: -32602, Message: "ROUTING_FAILED: " + err.Error()}
|
|
}
|
|
|
|
if res.Status == "unavailable" {
|
|
return o.formatUnavailable(res), nil
|
|
}
|
|
|
|
sessionID := shared.StringArg(params, "sessionId", "")
|
|
threadID := shared.StringArg(params, "threadId", sessionID)
|
|
turnID := fmt.Sprintf("turn-%d", time.Now().UnixNano())
|
|
|
|
sess := o.server.getOrCreateSession(sessionID, threadID)
|
|
sess.mu.Lock()
|
|
sess.target = res.TargetID
|
|
sess.provider = res.ProviderID
|
|
sess.mode = res.TargetID
|
|
prompt := strings.TrimSpace(shared.StringArg(params, "taskPrompt", ""))
|
|
if prompt != "" {
|
|
sess.history = append(sess.history, "USER: "+prompt)
|
|
}
|
|
sess.mu.Unlock()
|
|
|
|
o.server.emitSessionUpdate(notify, turnID, map[string]any{
|
|
"type": "status",
|
|
"event": "started",
|
|
"message": "session started",
|
|
"pending": true,
|
|
"error": false,
|
|
})
|
|
|
|
if res.TargetID == "gateway" {
|
|
result, rpcErr := o.runGateway(ctx, method, params, res, turnID, notify)
|
|
if rpcErr != nil {
|
|
return nil, rpcErr
|
|
}
|
|
return o.normalizeResult(sess, result, res, turnID, params), nil
|
|
}
|
|
|
|
compat, ok := o.server.providers[res.ProviderID]
|
|
if !ok {
|
|
return nil, &shared.RPCError{Code: -32001, Message: "PROVIDER_NOT_FOUND: " + res.ProviderID}
|
|
}
|
|
|
|
sess.mu.Lock()
|
|
sess.compat = compat
|
|
sess.mu.Unlock()
|
|
|
|
sink := func(update map[string]any) {
|
|
o.server.emitSessionUpdate(notify, turnID, update)
|
|
}
|
|
|
|
var result map[string]any
|
|
switch method {
|
|
case "session.start":
|
|
result, err = compat.StartSession(ctx, sessionID, threadID, params, sink)
|
|
case "session.message":
|
|
result, err = compat.SendMessage(ctx, sessionID, threadID, params, sink)
|
|
default:
|
|
err = fmt.Errorf("unsupported session method: %s", method)
|
|
}
|
|
if err != nil {
|
|
return nil, &shared.RPCError{Code: -32002, Message: "EXECUTION_FAILED: " + err.Error()}
|
|
}
|
|
|
|
return o.normalizeResult(sess, result, res, turnID, params), nil
|
|
}
|
|
|
|
func (o *SessionOrchestrator) runGateway(
|
|
ctx context.Context,
|
|
method string,
|
|
params map[string]any,
|
|
routing RoutingResult,
|
|
turnID string,
|
|
notify func(map[string]any),
|
|
) (map[string]any, *shared.RPCError) {
|
|
if o.server.gateway == nil {
|
|
return nil, &shared.RPCError{Code: -32001, Message: "GATEWAY_NOT_INITIALIZED"}
|
|
}
|
|
|
|
gatewayProvider := resolvedGatewayProviderID(params, routing)
|
|
if gatewayProvider == "" {
|
|
return nil, &shared.RPCError{Code: -32602, Message: "GATEWAY_PROVIDER_REQUIRED"}
|
|
}
|
|
params = withResolvedGatewayProvider(params, gatewayProvider)
|
|
result := o.server.gateway.RequestByMode(
|
|
gatewayProvider,
|
|
method,
|
|
params,
|
|
2*time.Minute,
|
|
notify,
|
|
)
|
|
if !result.OK {
|
|
errMessage := strings.TrimSpace(shared.StringArg(result.Error, "message", "gateway execution failed"))
|
|
return nil, &shared.RPCError{Code: -32002, Message: errMessage}
|
|
}
|
|
payload := shared.AsMap(result.Payload)
|
|
if len(payload) == 0 {
|
|
payload = map[string]any{
|
|
"success": true,
|
|
"turnId": turnID,
|
|
"mode": router.ExecutionTargetGatewayChat,
|
|
}
|
|
}
|
|
if _, ok := payload["turnId"]; !ok {
|
|
payload["turnId"] = turnID
|
|
}
|
|
if _, ok := payload["mode"]; !ok {
|
|
payload["mode"] = router.ExecutionTargetGatewayChat
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func resolvedGatewayProviderID(params map[string]any, routing RoutingResult) string {
|
|
for _, value := range []string{
|
|
routing.GatewayProviderID,
|
|
shared.StringArg(params, "gatewayProvider", ""),
|
|
shared.StringArg(params, "gatewayProviderId", ""),
|
|
} {
|
|
if provider := strings.TrimSpace(value); provider != "" {
|
|
return provider
|
|
}
|
|
}
|
|
routingParams := shared.AsMap(params["routing"])
|
|
for _, key := range []string{
|
|
"gatewayProvider",
|
|
"gatewayProviderId",
|
|
"preferredGatewayProviderId",
|
|
} {
|
|
if provider := strings.TrimSpace(shared.StringArg(routingParams, key, "")); provider != "" {
|
|
return provider
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func withResolvedGatewayProvider(params map[string]any, gatewayProvider string) map[string]any {
|
|
next := make(map[string]any, len(params)+2)
|
|
for key, value := range params {
|
|
next[key] = value
|
|
}
|
|
next["gatewayProvider"] = gatewayProvider
|
|
next["gatewayProviderId"] = gatewayProvider
|
|
return next
|
|
}
|
|
|
|
func (o *SessionOrchestrator) formatUnavailable(res RoutingResult) map[string]any {
|
|
return map[string]any{
|
|
"success": false,
|
|
"status": "unavailable",
|
|
"unavailable": true,
|
|
"unavailableCode": res.UnavailableCode,
|
|
"unavailableMessage": res.UnavailableMsg,
|
|
"resolvedExecutionTarget": res.TargetID,
|
|
"resolvedProviderId": res.ProviderID,
|
|
"resolvedGatewayProviderId": res.GatewayProviderID,
|
|
"resolvedModel": res.Model,
|
|
"resolvedSkills": append([]string(nil), res.Skills...),
|
|
}
|
|
}
|
|
|
|
func (o *SessionOrchestrator) normalizeResult(sess *session, result map[string]any, routing RoutingResult, turnID string, params map[string]any) map[string]any {
|
|
if result == nil {
|
|
result = map[string]any{}
|
|
}
|
|
|
|
successValue, hasSuccess := result["success"]
|
|
success := !hasSuccess || parseBool(successValue)
|
|
|
|
output := strings.TrimSpace(shared.StringArg(result, "output", ""))
|
|
if output == "" {
|
|
output = strings.TrimSpace(shared.StringArg(result, "summary", ""))
|
|
}
|
|
if output == "" && success {
|
|
output = strings.TrimSpace(shared.StringArg(result, "message", ""))
|
|
}
|
|
|
|
sess.mu.Lock()
|
|
if output != "" {
|
|
sess.history = append(sess.history, "ASSISTANT: "+output)
|
|
}
|
|
sess.mu.Unlock()
|
|
|
|
result["turnId"] = turnID
|
|
result["status"] = "completed"
|
|
if !hasSuccess {
|
|
result["success"] = true
|
|
}
|
|
result["resolvedExecutionTarget"] = routing.TargetID
|
|
result["resolvedProviderId"] = routing.ProviderID
|
|
result["resolvedGatewayProviderId"] = routing.GatewayProviderID
|
|
result["resolvedModel"] = routing.Model
|
|
result["resolvedSkills"] = append([]string(nil), routing.Skills...)
|
|
if output != "" {
|
|
result["output"] = output
|
|
if _, ok := result["summary"]; !ok {
|
|
result["summary"] = output
|
|
}
|
|
}
|
|
if output == "" && routing.TargetID != "gateway" && !parseBool(result["success"]) {
|
|
result["status"] = "failed"
|
|
} else if output == "" && routing.TargetID != "gateway" {
|
|
result["success"] = false
|
|
result["status"] = "failed"
|
|
result["error"] = "provider returned no displayable output"
|
|
result["message"] = "provider returned no displayable output"
|
|
}
|
|
|
|
workingDirectory := shared.StringArg(params, "workingDirectory", "")
|
|
routingParams := shared.AsMap(params["routing"])
|
|
routingMode := strings.TrimSpace(shared.StringArg(routingParams, "routingMode", ""))
|
|
if workingDirectory != "" && routingMode == "auto" {
|
|
_ = o.server.memoryService.RecordSuccess(workingDirectory, memory.SuccessEntry{
|
|
ResolvedExecutionTarget: routing.TargetID,
|
|
ResolvedProviderID: routing.ProviderID,
|
|
ResolvedModel: routing.Model,
|
|
ResolvedSkills: routing.Skills,
|
|
Summary: output,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (s *Server) getOrCreateSession(sessionID, threadID string) *session {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
sess, ok := s.sessions[sessionID]
|
|
if !ok {
|
|
sess = &session{
|
|
sessionID: sessionID,
|
|
threadID: threadID,
|
|
}
|
|
s.sessions[sessionID] = sess
|
|
}
|
|
return sess
|
|
}
|
|
|
|
func (s *Server) emitSessionUpdate(notify func(map[string]any), turnID string, update map[string]any) {
|
|
if notify == nil || update == nil {
|
|
return
|
|
}
|
|
update["turnId"] = turnID
|
|
notify(shared.NotificationEnvelope("session.update", update))
|
|
}
|